about summary refs log tree commit diff stats
path: root/apworld/client
diff options
context:
space:
mode:
Diffstat (limited to 'apworld/client')
-rw-r--r--apworld/client/allowNumbers.gd10
-rw-r--r--apworld/client/animationListener.gd38
-rw-r--r--apworld/client/apworld_runtime.gd49
-rw-r--r--apworld/client/assets/goal.pngbin0 -> 215 bytes
-rw-r--r--apworld/client/assets/location.pngbin0 -> 311 bytes
-rw-r--r--apworld/client/assets/worldport.pngbin0 -> 219 bytes
-rw-r--r--apworld/client/client.gd318
-rw-r--r--apworld/client/collectable.gd16
-rw-r--r--apworld/client/compass.gd66
-rw-r--r--apworld/client/compass_overlay.gd17
-rw-r--r--apworld/client/door.gd75
-rw-r--r--apworld/client/effects.gd32
-rw-r--r--apworld/client/gamedata.gd337
-rw-r--r--apworld/client/keyHolder.gd38
-rw-r--r--apworld/client/keyHolderChecker.gd24
-rw-r--r--apworld/client/keyHolderResetterListener.gd10
-rw-r--r--apworld/client/keyboard.gd228
-rw-r--r--apworld/client/locationListener.gd20
-rw-r--r--apworld/client/main.gd314
-rw-r--r--apworld/client/manager.gd778
-rw-r--r--apworld/client/maps/control_center.gd143
-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_gallery.gd7
-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/messages.gd74
-rw-r--r--apworld/client/minimap.gd178
-rw-r--r--apworld/client/painting.gd38
-rw-r--r--apworld/client/paintingAuto.gd43
-rw-r--r--apworld/client/panel.gd101
-rw-r--r--apworld/client/pauseMenu.gd110
-rw-r--r--apworld/client/player.gd222
-rw-r--r--apworld/client/rainbowText.gd10
-rw-r--r--apworld/client/rteMenu.gd67
-rw-r--r--apworld/client/run_from_apworld.tscn30
-rw-r--r--apworld/client/run_from_source.tscn22
-rw-r--r--apworld/client/saver.gd23
-rw-r--r--apworld/client/settings_screen.gd149
-rw-r--r--apworld/client/source_runtime.gd33
-rw-r--r--apworld/client/teleport.gd38
-rw-r--r--apworld/client/teleportListener.gd49
-rw-r--r--apworld/client/textclient.gd514
-rw-r--r--apworld/client/unlockReaderListener.gd46
-rw-r--r--apworld/client/vendor/LICENSE21
-rw-r--r--apworld/client/vendor/WebSocketServer.gd173
-rw-r--r--apworld/client/victoryListener.gd20
-rw-r--r--apworld/client/visibilityListener.gd38
-rw-r--r--apworld/client/worldport.gd61
-rw-r--r--apworld/client/worldportListener.gd8
59 files changed, 5138 insertions, 0 deletions
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/animationListener.gd b/apworld/client/animationListener.gd new file mode 100644 index 0000000..c3b26db --- /dev/null +++ b/apworld/client/animationListener.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/listeners/animationListener.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
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/apworld/client/apworld_runtime.gd b/apworld/client/apworld_runtime.gd new file mode 100644 index 0000000..03568bf --- /dev/null +++ b/apworld/client/apworld_runtime.gd
@@ -0,0 +1,49 @@
1extends Node
2
3var apworld_reader
4
5
6func _init(path):
7 apworld_reader = ZIPReader.new()
8 apworld_reader.open(path)
9
10
11func _get_true_path(path):
12 if path.begins_with("../"):
13 return "lingo2/%s" % path.substr(3)
14 else:
15 return "lingo2/client/%s" % path
16
17
18func path_exists(path):
19 var true_path = _get_true_path(path)
20 return apworld_reader.file_exists(true_path)
21
22
23func load_script(path):
24 var true_path = _get_true_path(path)
25
26 var script = GDScript.new()
27 script.source_code = apworld_reader.read_file(true_path).get_string_from_utf8()
28 script.reload()
29
30 return script
31
32
33func read_path(path):
34 var true_path = _get_true_path(path)
35 return apworld_reader.read_file(true_path)
36
37
38func load_script_as_scene(path, scene_name):
39 var script = load_script(path)
40 var instance = script.new()
41 instance.name = scene_name
42
43 get_tree().unload_current_scene()
44 _load_scene.call_deferred(instance)
45
46
47func _load_scene(instance):
48 get_tree().get_root().add_child(instance)
49 get_tree().current_scene = instance
diff --git a/apworld/client/assets/goal.png b/apworld/client/assets/goal.png new file mode 100644 index 0000000..bd1650d --- /dev/null +++ b/apworld/client/assets/goal.png
Binary files differ
diff --git a/apworld/client/assets/location.png b/apworld/client/assets/location.png new file mode 100644 index 0000000..5304deb --- /dev/null +++ b/apworld/client/assets/location.png
Binary files differ
diff --git a/apworld/client/assets/worldport.png b/apworld/client/assets/worldport.png new file mode 100644 index 0000000..19dfdc3 --- /dev/null +++ b/apworld/client/assets/worldport.png
Binary files differ
diff --git a/apworld/client/client.gd b/apworld/client/client.gd new file mode 100644 index 0000000..c149482 --- /dev/null +++ b/apworld/client/client.gd
@@ -0,0 +1,318 @@
1extends Node
2
3const ap_version = {"major": 0, "minor": 6, "build": 3, "class": "Version"}
4
5var SCRIPT_websocketserver
6
7var _server
8var _should_process = false
9
10var _remote_version = {"major": 0, "minor": 0, "build": 0}
11var _gen_version = {"major": 0, "minor": 0, "build": 0}
12
13var ap_server = ""
14var ap_user = ""
15var ap_pass = ""
16
17var _seed = ""
18var _team = 0
19var _slot = 0
20var _checked_locations = []
21var _checked_worldports = []
22var _received_indexes = []
23var _received_items = {}
24var _slot_data = {}
25var _accessible_locations = []
26var _accessible_worldports = []
27var _goal_accessible = false
28var _latched_doors = []
29var _hinted_locations = []
30
31signal could_not_connect
32signal connect_status
33signal client_connected(slot_data)
34signal item_received(item, amount)
35signal location_scout_received(location_id, item_name, player_name, flags, for_self)
36signal text_message_received(message)
37signal item_sent_notification(message)
38signal hint_received(message)
39signal door_latched(id)
40signal accessible_locations_updated
41signal checked_locations_updated
42signal ignored_locations_updated(locations)
43signal checked_worldports_updated
44signal keyboard_update_received
45signal hinted_locations_updated
46
47
48func _init():
49 set_process_mode(Node.PROCESS_MODE_ALWAYS)
50
51 global._print("Instantiated APClient")
52
53
54func _ready():
55 _server = SCRIPT_websocketserver.new()
56 _server.client_connected.connect(_on_web_socket_server_client_connected)
57 _server.client_disconnected.connect(_on_web_socket_server_client_disconnected)
58 _server.message_received.connect(_on_web_socket_server_message_received)
59 add_child(_server)
60 _server.listen(43182)
61
62
63func _reset_state():
64 _should_process = false
65 _received_items = {}
66 _received_indexes = []
67 _checked_worldports = []
68 _accessible_locations = []
69 _accessible_worldports = []
70 _goal_accessible = false
71
72
73func disconnect_from_ap():
74 sendMessage([{"cmd": "Disconnect"}])
75
76
77func _on_web_socket_server_client_connected(peer_id: int) -> void:
78 var peer: WebSocketPeer = _server.peers[peer_id]
79 print("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()])
80 _server.send(-peer_id, "[%d] connected" % peer_id)
81
82
83func _on_web_socket_server_client_disconnected(peer_id: int) -> void:
84 var peer: WebSocketPeer = _server.peers[peer_id]
85 print(
86 (
87 "Remote client disconnected: %d. Code: %d, Reason: %s"
88 % [peer_id, peer.get_close_code(), peer.get_close_reason()]
89 )
90 )
91 _server.send(-peer_id, "[%d] disconnected" % peer_id)
92
93
94func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> void:
95 global._print("Got data from server: " + packet)
96 var json = JSON.new()
97 var jserror = json.parse(packet)
98 if jserror != OK:
99 global._print("Error parsing packet from AP: " + jserror.error_string)
100 return
101
102 for message in json.data:
103 var cmd = message["cmd"]
104 global._print("Received command: " + cmd)
105
106 if cmd == "Connected":
107 _seed = message["seed_name"]
108 _remote_version = message["version"]
109 _gen_version = message["generator_version"]
110 _team = message["team"]
111 _slot = message["slot"]
112 _slot_data = message["slot_data"]
113
114 _checked_locations = []
115 for location in message["checked_locations"]:
116 _checked_locations.append(int(location))
117
118 client_connected.emit(_slot_data)
119
120 elif cmd == "ConnectionRefused":
121 could_not_connect.emit(message["text"])
122 global._print("Connection to AP refused")
123
124 elif cmd == "UpdateLocations":
125 for location in message["locations"]:
126 var lint = int(location)
127 if not _checked_locations.has(lint):
128 _checked_locations.append(lint)
129
130 checked_locations_updated.emit()
131
132 elif cmd == "UpdateWorldports":
133 for port_id in message["worldports"]:
134 var lint = int(port_id)
135 if not _checked_worldports.has(lint):
136 _checked_worldports.append(lint)
137
138 checked_worldports_updated.emit()
139
140 elif cmd == "ItemReceived":
141 for item in message["items"]:
142 var index = int(item["index"])
143 if _received_indexes.has(index):
144 # Do not re-process items.
145 continue
146
147 _received_indexes.append(index)
148
149 var item_id = int(item["id"])
150 _received_items[item_id] = _received_items.get(item_id, 0) + 1
151
152 item_received.emit(item, _received_items[item_id])
153
154 elif cmd == "TextMessage":
155 text_message_received.emit(message["data"])
156
157 elif cmd == "ItemSentNotif":
158 item_sent_notification.emit(message)
159
160 elif cmd == "HintReceived":
161 hint_received.emit(message)
162
163 elif cmd == "LocationInfo":
164 for loc in message["locations"]:
165 location_scout_received.emit(
166 int(loc["id"]), loc["item"], loc["player"], int(loc["flags"]), int(loc["self"])
167 )
168
169 elif cmd == "AccessibleLocations":
170 _accessible_locations.clear()
171 _accessible_worldports.clear()
172
173 for loc in message["locations"]:
174 _accessible_locations.append(int(loc))
175
176 if "worldports" in message:
177 for port_id in message["worldports"]:
178 _accessible_worldports.append(int(port_id))
179
180 _goal_accessible = bool(message.get("goal", false))
181
182 accessible_locations_updated.emit()
183
184 elif cmd == "UpdateKeyboard":
185 var updates = {}
186 for k in message["updates"]:
187 updates[k] = int(message["updates"][k])
188
189 keyboard_update_received.emit(updates)
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
220
221func connectToServer(server, un, pw):
222 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
223
224 ap_server = server
225 ap_user = un
226 ap_pass = pw
227
228 _should_process = true
229
230 connect_status.emit("Connecting...")
231
232
233func sendMessage(msg):
234 var payload = JSON.stringify(msg)
235 _server.send(0, payload)
236
237
238func connectToRoom():
239 connect_status.emit("Authenticating...")
240
241 sendMessage(
242 [
243 {
244 "cmd": "Connect",
245 "password": ap_pass,
246 "game": "Lingo 2",
247 "name": ap_user,
248 }
249 ]
250 )
251
252
253func requestSync():
254 sendMessage([{"cmd": "Sync"}])
255
256
257func sendLocation(loc_id):
258 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
259
260
261func sendLocations(loc_ids):
262 sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}])
263
264
265func say(textdata):
266 sendMessage([{"cmd": "Say", "text": textdata}])
267
268
269func completedGoal():
270 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
271
272
273func scoutLocations(loc_ids):
274 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
275
276
277func updateKeyboard(updates):
278 sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}])
279
280
281func checkWorldport(port_id):
282 if not _checked_worldports.has(port_id):
283 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}])
284
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
309func sendQuit():
310 sendMessage([{"cmd": "Quit"}])
311
312
313func hasItem(item_id):
314 return _received_items.has(item_id)
315
316
317func getItemAmount(item_id):
318 return _received_items.get(item_id, 0)
diff --git a/apworld/client/collectable.gd b/apworld/client/collectable.gd new file mode 100644 index 0000000..4a17a2a --- /dev/null +++ b/apworld/client/collectable.gd
@@ -0,0 +1,16 @@
1extends "res://scripts/nodes/collectable.gd"
2
3
4func pickedUp():
5 if unlock_type == "key":
6 var ap = global.get_node("Archipelago")
7 if ap.get_letter_behavior(unlock_key, level == 2) == ap.kLETTER_BEHAVIOR_VANILLA:
8 ap.keyboard.collect_local_letter(unlock_key, level)
9 else:
10 ap.keyboard.update_unlocks()
11
12 super.pickedUp()
13
14
15func setScoutedText(text):
16 get_node("MeshInstance3D").mesh.text = text.replace(" ", "\n")
diff --git a/apworld/client/compass.gd b/apworld/client/compass.gd new file mode 100644 index 0000000..c90475a --- /dev/null +++ b/apworld/client/compass.gd
@@ -0,0 +1,66 @@
1extends Node2D
2
3const RADIUS = 48
4
5var _font
6
7
8func _ready():
9 _font = load("res://assets/fonts/Lingo2.ttf")
10
11
12func _draw():
13 draw_circle(Vector2.ZERO, RADIUS, Color(1.0, 1.0, 1.0, 0.8), true)
14 draw_circle(Vector2.ZERO, RADIUS, Color.BLACK, false)
15 draw_string(
16 _font,
17 Vector2(-4, -RADIUS * 3.0 / 4.0),
18 "N",
19 HorizontalAlignment.HORIZONTAL_ALIGNMENT_LEFT,
20 -1,
21 16,
22 Color.BLACK
23 )
24 draw_set_transform(Vector2.ZERO, PI / 2)
25 draw_string(
26 _font,
27 Vector2(-4, -RADIUS * 3.0 / 4.0),
28 "E",
29 HorizontalAlignment.HORIZONTAL_ALIGNMENT_LEFT,
30 -1,
31 16,
32 Color.BLACK
33 )
34 draw_set_transform(Vector2.ZERO, PI)
35 draw_string(
36 _font,
37 Vector2(-4, -RADIUS * 3.0 / 4.0),
38 "S",
39 HorizontalAlignment.HORIZONTAL_ALIGNMENT_LEFT,
40 -1,
41 16,
42 Color.BLACK
43 )
44 draw_set_transform(Vector2.ZERO, PI * 3.0 / 2.0)
45 draw_string(
46 _font,
47 Vector2(-4, -RADIUS * 3.0 / 4.0),
48 "W",
49 HorizontalAlignment.HORIZONTAL_ALIGNMENT_LEFT,
50 -1,
51 16,
52 Color.BLACK
53 )
54 draw_set_transform(Vector2.ZERO)
55 draw_colored_polygon(
56 PackedVector2Array(
57 [Vector2(0, -RADIUS * 5.0 / 8.0), Vector2(-RADIUS / 6.0, 0), Vector2(RADIUS / 6.0, 0)]
58 ),
59 Color.RED
60 )
61 draw_colored_polygon(
62 PackedVector2Array(
63 [Vector2(0, RADIUS * 5.0 / 8.0), Vector2(-RADIUS / 6.0, 0), Vector2(RADIUS / 6.0, 0)]
64 ),
65 Color.GRAY
66 )
diff --git a/apworld/client/compass_overlay.gd b/apworld/client/compass_overlay.gd new file mode 100644 index 0000000..56e81ff --- /dev/null +++ b/apworld/client/compass_overlay.gd
@@ -0,0 +1,17 @@
1extends CanvasLayer
2
3var SCRIPT_compass
4
5var compass
6
7
8func _ready():
9 compass = SCRIPT_compass.new()
10 compass.position = Vector2(1840, 80)
11 add_child(compass)
12
13 visible = false
14
15
16func update_rotation(ry):
17 compass.rotation = ry
diff --git a/apworld/client/door.gd b/apworld/client/door.gd new file mode 100644 index 0000000..63cfa99 --- /dev/null +++ b/apworld/client/door.gd
@@ -0,0 +1,75 @@
1extends "res://scripts/nodes/door.gd"
2
3var door_id
4var item_id
5var item_amount
6var latched = false
7
8
9func _ready():
10 var node_path = String(
11 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
12 )
13
14 var gamedata = global.get_node("Gamedata")
15 door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
16 if door_id != null:
17 var ap = global.get_node("Archipelago")
18 var item_lock = ap.get_item_id_for_door(door_id)
19
20 if item_lock != null:
21 item_id = item_lock[0]
22 item_amount = item_lock[1]
23
24 self.senders = []
25 self.senderGroup = []
26 self.nested = false
27 self.complete_at = 0
28 self.max_length = 0
29 self.excludeSenders = []
30
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
38
39 if global.map == "the_sun_temple":
40 if name == "spe_EndPlatform" or name == "spe_entry_2":
41 senders = [NodePath("/root/scene/Panels/EndCheck_dog")]
42
43 if global.map == "the_parthenon":
44 if name == "spe_entry_1":
45 senders = [NodePath("/root/scene/Panels/EndCheck_dog")]
46
47 super._ready()
48
49
50func _readier():
51 var ap = global.get_node("Archipelago")
52
53 if ap.client.getItemAmount(item_id) >= item_amount:
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 new file mode 100644 index 0000000..a2e023c --- /dev/null +++ b/apworld/client/gamedata.gd
@@ -0,0 +1,337 @@
1extends Node
2
3var SCRIPT_proto
4
5var objects
6var door_id_by_map_node_path = {}
7var painting_id_by_map_node_path = {}
8var panel_id_by_map_node_path = {}
9var port_id_by_map_node_path = {}
10var door_id_by_ap_id = {}
11var map_id_by_name = {}
12var progressive_id_by_ap_id = {}
13var letter_id_by_ap_id = {}
14var symbol_item_ids = []
15var anti_trap_ids = {}
16var location_name_by_id = {}
17var map_id_by_location_id = {}
18var ending_display_name_by_name = {}
19var port_id_by_ap_id = {}
20var map_id_by_rte_ap_id = {}
21
22var kSYMBOL_ITEMS
23
24
25func _init(proto_script):
26 SCRIPT_proto = proto_script
27
28 kSYMBOL_ITEMS = {
29 SCRIPT_proto.PuzzleSymbol.SUN: "Sun Symbol",
30 SCRIPT_proto.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
31 SCRIPT_proto.PuzzleSymbol.ZERO: "Zero Symbol",
32 SCRIPT_proto.PuzzleSymbol.EXAMPLE: "Example Symbol",
33 SCRIPT_proto.PuzzleSymbol.BOXES: "Boxes Symbol",
34 SCRIPT_proto.PuzzleSymbol.PLANET: "Planet Symbol",
35 SCRIPT_proto.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
36 SCRIPT_proto.PuzzleSymbol.CROSS: "Cross Symbol",
37 SCRIPT_proto.PuzzleSymbol.SWEET: "Sweet Symbol",
38 SCRIPT_proto.PuzzleSymbol.GENDER: "Gender Symbol",
39 SCRIPT_proto.PuzzleSymbol.AGE: "Age Symbol",
40 SCRIPT_proto.PuzzleSymbol.SOUND: "Sound Symbol",
41 SCRIPT_proto.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
42 SCRIPT_proto.PuzzleSymbol.JOB: "Job Symbol",
43 SCRIPT_proto.PuzzleSymbol.STARS: "Stars Symbol",
44 SCRIPT_proto.PuzzleSymbol.NULL: "Null Symbol",
45 SCRIPT_proto.PuzzleSymbol.EVAL: "Eval Symbol",
46 SCRIPT_proto.PuzzleSymbol.LINGO: "Lingo Symbol",
47 SCRIPT_proto.PuzzleSymbol.QUESTION: "Question Symbol",
48 }
49
50
51func load(data_bytes):
52 objects = SCRIPT_proto.AllObjects.new()
53
54 var result_code = objects.from_bytes(data_bytes)
55 if result_code != SCRIPT_proto.PB_ERR.NO_ERRORS:
56 print("Could not load generated data: %d" % result_code)
57 return
58
59 for map in objects.get_maps():
60 map_id_by_name[map.get_name()] = map.get_id()
61
62 if map.has_rte_ap_id():
63 map_id_by_rte_ap_id[map.get_rte_ap_id()] = map.get_id()
64
65 for door in objects.get_doors():
66 var map = objects.get_maps()[door.get_map_id()]
67
68 if not map.get_name() in door_id_by_map_node_path:
69 door_id_by_map_node_path[map.get_name()] = {}
70
71 var map_data = door_id_by_map_node_path[map.get_name()]
72 for receiver in door.get_receivers():
73 map_data[receiver] = door.get_id()
74
75 for painting_id in door.get_move_paintings():
76 var painting = objects.get_paintings()[painting_id]
77 map_data[painting.get_path()] = door.get_id()
78
79 if door.has_ap_id():
80 door_id_by_ap_id[door.get_ap_id()] = door.get_id()
81
82 if (
83 door.get_type() == SCRIPT_proto.DoorType.STANDARD
84 or door.get_type() == SCRIPT_proto.DoorType.LOCATION_ONLY
85 or door.get_type() == SCRIPT_proto.DoorType.GRAVESTONE
86 ):
87 location_name_by_id[door.get_ap_id()] = _get_door_location_name(door)
88 map_id_by_location_id[door.get_ap_id()] = map.get_id()
89
90 for painting in objects.get_paintings():
91 var room = objects.get_rooms()[painting.get_room_id()]
92 var map = objects.get_maps()[room.get_map_id()]
93
94 if not map.get_name() in painting_id_by_map_node_path:
95 painting_id_by_map_node_path[map.get_name()] = {}
96
97 var _map_data = painting_id_by_map_node_path[map.get_name()]
98
99 for port in objects.get_ports():
100 var room = objects.get_rooms()[port.get_room_id()]
101 var map = objects.get_maps()[room.get_map_id()]
102
103 if not map.get_name() in port_id_by_map_node_path:
104 port_id_by_map_node_path[map.get_name()] = {}
105
106 var map_data = port_id_by_map_node_path[map.get_name()]
107 map_data[port.get_path()] = port.get_id()
108
109 if port.has_ap_id():
110 port_id_by_ap_id[port.get_ap_id()] = port.get_id()
111
112 for progressive in objects.get_progressives():
113 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
114
115 for letter in objects.get_letters():
116 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
117 location_name_by_id[letter.get_ap_id()] = _get_letter_location_name(letter)
118
119 var room = objects.get_rooms()[letter.get_room_id()]
120 map_id_by_location_id[letter.get_ap_id()] = room.get_map_id()
121
122 for mastery in objects.get_masteries():
123 location_name_by_id[mastery.get_ap_id()] = _get_mastery_location_name(mastery)
124
125 var room = objects.get_rooms()[mastery.get_room_id()]
126 map_id_by_location_id[mastery.get_ap_id()] = room.get_map_id()
127
128 for ending in objects.get_endings():
129 var location_name = _get_ending_location_name(ending)
130 location_name_by_id[ending.get_ap_id()] = location_name
131 ending_display_name_by_name[ending.get_name()] = location_name
132
133 var room = objects.get_rooms()[ending.get_room_id()]
134 map_id_by_location_id[ending.get_ap_id()] = room.get_map_id()
135
136 for keyholder in objects.get_keyholders():
137 if keyholder.has_key():
138 location_name_by_id[keyholder.get_ap_id()] = _get_keyholder_location_name(keyholder)
139
140 var room = objects.get_rooms()[keyholder.get_room_id()]
141 map_id_by_location_id[keyholder.get_ap_id()] = room.get_map_id()
142
143 for panel in objects.get_panels():
144 var room = objects.get_rooms()[panel.get_room_id()]
145 var map = objects.get_maps()[room.get_map_id()]
146
147 if not map.get_name() in panel_id_by_map_node_path:
148 panel_id_by_map_node_path[map.get_name()] = {}
149
150 var map_data = panel_id_by_map_node_path[map.get_name()]
151 map_data[panel.get_path()] = panel.get_id()
152
153 for symbol_name in kSYMBOL_ITEMS.values():
154 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
155
156 for special_name in objects.get_special_ids().keys():
157 if special_name.begins_with("Anti "):
158 anti_trap_ids[objects.get_special_ids()[special_name]] = (
159 special_name.substr(5).to_lower()
160 )
161
162
163func get_door_for_map_node_path(map_name, node_path):
164 if not door_id_by_map_node_path.has(map_name):
165 return null
166
167 var map_data = door_id_by_map_node_path[map_name]
168 return map_data.get(node_path, null)
169
170
171func get_panel_for_map_node_path(map_name, node_path):
172 if not panel_id_by_map_node_path.has(map_name):
173 return null
174
175 var map_data = panel_id_by_map_node_path[map_name]
176 return map_data.get(node_path, null)
177
178
179func get_port_for_map_node_path(map_name, node_path):
180 if not port_id_by_map_node_path.has(map_name):
181 return null
182
183 var map_data = port_id_by_map_node_path[map_name]
184 return map_data.get(node_path, null)
185
186
187func get_door_ap_id(door_id):
188 var door = objects.get_doors()[door_id]
189 if door.has_ap_id():
190 return door.get_ap_id()
191 else:
192 return null
193
194
195func get_door_map_name(door_id):
196 var door = objects.get_doors()[door_id]
197 var map = objects.get_maps()[door.get_map_id()]
198 return map.get_name()
199
200
201func get_door_receivers(door_id):
202 var door = objects.get_doors()[door_id]
203 return door.get_receivers()
204
205
206func get_worldport_display_name(port_id):
207 var port = objects.get_ports()[port_id]
208 return "%s - %s" % [_get_room_object_map_name(port), port.get_display_name()]
209
210
211func get_map_name_for_location(location_id):
212 var map_id = map_id_by_location_id[location_id]
213 var map = objects.get_maps()[map_id]
214 return map.get_name()
215
216
217func get_map_name_for_port(port_id):
218 var port = objects.get_ports()[port_id]
219 var room = objects.get_rooms()[port.get_room_id()]
220 var map = objects.get_maps()[room.get_map_id()]
221 return map.get_name()
222
223
224func _get_map_object_map_name(obj):
225 return objects.get_maps()[obj.get_map_id()].get_display_name()
226
227
228func _get_room_object_map_name(obj):
229 return _get_map_object_map_name(objects.get_rooms()[obj.get_room_id()])
230
231
232func _get_room_object_location_prefix(obj):
233 var room = objects.get_rooms()[obj.get_room_id()]
234 var game_map = objects.get_maps()[room.get_map_id()]
235
236 if room.has_panel_display_name():
237 return "%s (%s)" % [game_map.get_display_name(), room.get_panel_display_name()]
238 else:
239 return game_map.get_display_name()
240
241
242func _get_door_location_name(door):
243 var map_part = _get_room_object_location_prefix(door)
244
245 if door.has_location_name():
246 return "%s - %s" % [map_part, door.get_location_name()]
247
248 var generated_location_name = _get_generated_door_location_name(door)
249 if generated_location_name != null:
250 return generated_location_name
251
252 return "%s - %s" % [map_part, door.get_name()]
253
254
255func _get_generated_door_location_name(door):
256 if door.get_type() != SCRIPT_proto.DoorType.STANDARD:
257 return null
258
259 if (
260 door.get_keyholders().size() > 0
261 or (door.has_white_ending() and door.get_white_ending())
262 or door.has_complete_at()
263 ):
264 return null
265
266 if door.get_panels().size() > 4:
267 return null
268
269 var map_areas = []
270 for panel_id in door.get_panels():
271 var panel = objects.get_panels()[panel_id.get_panel()]
272 var panel_room = objects.get_rooms()[panel.get_room_id()]
273 # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas.
274 var panel_display_name = ""
275 if panel_room.has_panel_display_name():
276 panel_display_name = panel_room.get_panel_display_name()
277 if not map_areas.has(panel_display_name):
278 map_areas.append(panel_display_name)
279
280 if map_areas.size() > 1:
281 return null
282
283 var game_map = objects.get_maps()[door.get_map_id()]
284 var map_area = map_areas[0]
285 var map_part
286 if map_area == "":
287 map_part = game_map.get_display_name()
288 else:
289 map_part = "%s (%s)" % [game_map.get_display_name(), map_area]
290
291 var panel_names = []
292 for panel_id in door.get_panels():
293 var panel_data = objects.get_panels()[panel_id.get_panel()]
294 var panel_name
295 if panel_data.has_display_name():
296 panel_name = panel_data.get_display_name()
297 else:
298 panel_name = panel_data.get_name()
299
300 var location_part
301 if panel_id.has_answer():
302 location_part = "%s/%s" % [panel_name, panel_id.get_answer().to_upper()]
303 else:
304 location_part = panel_name
305
306 panel_names.append(location_part)
307
308 panel_names.sort()
309
310 return map_part + " - " + ", ".join(panel_names)
311
312
313func _get_letter_location_name(letter):
314 var letter_level = 2 if (letter.has_level2() and letter.get_level2()) else 1
315 var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level]
316 return "%s - %s" % [_get_room_object_map_name(letter), letter_name]
317
318
319func _get_mastery_location_name(mastery):
320 return "%s - Mastery" % _get_room_object_map_name(mastery)
321
322
323func _get_ending_location_name(ending):
324 return (
325 "%s - %s Ending" % [_get_room_object_map_name(ending), ending.get_name().to_pascal_case()]
326 )
327
328
329func _get_keyholder_location_name(keyholder):
330 return (
331 "%s - %s Keyholder"
332 % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()]
333 )
334
335
336func vec3d_to_vector3(input) -> Vector3:
337 return Vector3(input.get_x(), input.get_y(), input.get_z())
diff --git a/apworld/client/keyHolder.gd b/apworld/client/keyHolder.gd new file mode 100644 index 0000000..3c037ff --- /dev/null +++ b/apworld/client/keyHolder.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/keyHolder.gd"
2
3
4func setFromAp(key, level):
5 if level > 0:
6 has_key = true
7 is_complete = "%s%d" % [key, level]
8 held_key = key
9 held_level = level
10 get_node("Hinge/Letter").mesh.text = held_key
11 get_node("Hinge/Letter2").mesh.text = held_key
12 setMaterial()
13 emit_signal("trigger")
14 else:
15 has_key = false
16 held_key = ""
17 held_level = 0
18 setMaterial()
19 get_node("Hinge/Letter").mesh.text = "-"
20 get_node("Hinge/Letter2").mesh.text = "-"
21 is_complete = ""
22 emit_signal("untrigger")
23
24
25func addKey(key):
26 var node_path = String(
27 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
28 )
29 var ap = global.get_node("Archipelago")
30 ap.keyboard.put_in_keyholder(key, global.map, node_path)
31
32
33func removeKey():
34 var node_path = String(
35 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
36 )
37 var ap = global.get_node("Archipelago")
38 ap.keyboard.remove_from_keyholder(held_key, global.map, node_path)
diff --git a/apworld/client/keyHolderChecker.gd b/apworld/client/keyHolderChecker.gd new file mode 100644 index 0000000..a75a9e4 --- /dev/null +++ b/apworld/client/keyHolderChecker.gd
@@ -0,0 +1,24 @@
1extends "res://scripts/nodes/listeners/keyHolderChecker.gd"
2
3
4func check():
5 var ap = global.get_node("Archipelago")
6 var matches = []
7 for map in ap.keyboard.keyholder_state.keys():
8 var nodes = ap.keyboard.keyholder_state[map]
9 for node in nodes.keys():
10 matches.append([nodes[node], 1, map, "/root/scene/%s" % node])
11
12 var count = 0
13 for key_match in matches:
14 var active = (
15 key_match[2] + String(key_match[3]).replace("/root/scene/Components/KeyHolders/", ".")
16 )
17 if map[active] == key_match[0]:
18 emit_signal("trigger_letter", key_match[0], true)
19 count += 1
20 else:
21 emit_signal("trigger_letter", key_match[0], false)
22
23 if count > 25:
24 emit_signal("trigger")
diff --git a/apworld/client/keyHolderResetterListener.gd b/apworld/client/keyHolderResetterListener.gd new file mode 100644 index 0000000..9ab45f9 --- /dev/null +++ b/apworld/client/keyHolderResetterListener.gd
@@ -0,0 +1,10 @@
1extends "res://scripts/nodes/listeners/keyHolderResetterListener.gd"
2
3
4func reset():
5 var ap = global.get_node("Archipelago")
6 var was_removed = ap.keyboard.reset_keyholders()
7 if was_removed:
8 sfxPlayer.sfx_play("pickup")
9
10 ap.client.requestSync()
diff --git a/apworld/client/keyboard.gd b/apworld/client/keyboard.gd new file mode 100644 index 0000000..9026c06 --- /dev/null +++ b/apworld/client/keyboard.gd
@@ -0,0 +1,228 @@
1extends Node
2
3const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz"
4
5var letters_saved = {}
6var letters_in_keyholders = []
7var letters_blocked = []
8var letters_dynamic = {}
9var keyholder_state = {}
10
11var filename = ""
12
13
14func _init():
15 reset()
16
17
18func reset():
19 letters_saved.clear()
20 letters_in_keyholders.clear()
21 letters_blocked.clear()
22 letters_dynamic.clear()
23 keyholder_state.clear()
24
25
26func load_seed():
27 var ap = global.get_node("Archipelago")
28
29 reset()
30
31 filename = "user://archipelago_keys/%s_%d" % [ap.client._seed, ap.client._slot]
32
33 if FileAccess.file_exists(filename):
34 var ap_file = FileAccess.open(filename, FileAccess.READ)
35 var localdata = []
36 if ap_file != null:
37 localdata = ap_file.get_var(true)
38 ap_file.close()
39
40 if typeof(localdata) != TYPE_ARRAY:
41 print("AP keyboard file is corrupted")
42 localdata = []
43
44 if localdata.size() > 0:
45 letters_saved = localdata[0]
46 if localdata.size() > 1:
47 letters_in_keyholders = localdata[1]
48 if localdata.size() > 2:
49 keyholder_state = localdata[2]
50
51 if not letters_saved.is_empty():
52 ap.client.updateKeyboard(letters_saved)
53
54 for k in kALL_LETTERS:
55 var level = 0
56
57 if ap.get_letter_behavior(k, false) == ap.kLETTER_BEHAVIOR_UNLOCKED:
58 level += 1
59 if ap.get_letter_behavior(k, true) == ap.kLETTER_BEHAVIOR_UNLOCKED:
60 level += 1
61
62 letters_dynamic[k] = level
63
64 update_unlocks()
65
66
67func save():
68 var dir = DirAccess.open("user://")
69 var folder = "archipelago_keys"
70 if not dir.dir_exists(folder):
71 dir.make_dir(folder)
72
73 var file = FileAccess.open(filename, FileAccess.WRITE)
74
75 var data = [
76 letters_saved,
77 letters_in_keyholders,
78 keyholder_state,
79 ]
80 file.store_var(data, true)
81 file.close()
82
83
84func update_unlocks():
85 unlocks.resetKeys()
86
87 var has_doubles = false
88
89 for k in kALL_LETTERS:
90 var level = 0
91
92 if not letters_in_keyholders.has(k):
93 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0)
94
95 if level >= 2:
96 level = 2
97 has_doubles = true
98
99 if letters_blocked.has(k):
100 level = 0
101
102 unlocks.unlockKey(k, level)
103
104 if has_doubles and unlocks.data["double_letters"] != "unlocked":
105 var ap = global.get_node("Archipelago")
106 if ap.cyan_door_behavior == ap.kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER:
107 unlocks.setData("double_letters", "unlocked")
108
109
110func collect_local_letter(key, level):
111 var ap = global.get_node("Archipelago")
112 var true_level = 0
113
114 if ap.get_letter_behavior(key, false) == ap.kLETTER_BEHAVIOR_VANILLA:
115 true_level += 1
116 if level == 2 and ap.get_letter_behavior(key, true) == ap.kLETTER_BEHAVIOR_VANILLA:
117 true_level += 1
118
119 if true_level < letters_saved.get(key, 0):
120 return
121
122 letters_saved[key] = true_level
123
124 ap.client.updateKeyboard({key: true_level})
125
126 if letters_blocked.has(key):
127 letters_blocked.erase(key)
128
129 update_unlocks()
130 save()
131
132
133func collect_remote_letter(key, level):
134 if level < 0 or level > 2 or level < letters_dynamic.get(key, 0):
135 return
136
137 letters_dynamic[key] = level
138
139 if letters_blocked.has(key):
140 letters_blocked.erase(key)
141
142 update_unlocks()
143 save()
144
145
146func put_in_keyholder(key, map, kh_path):
147 if not keyholder_state.has(map):
148 keyholder_state[map] = {}
149
150 keyholder_state[map][kh_path] = key
151 letters_in_keyholders.append(key)
152
153 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(
154 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
155 )
156
157 update_unlocks()
158 save()
159
160
161func remove_from_keyholder(key, map, kh_path):
162 if not keyholder_state.has(map):
163 # This... shouldn't happen.
164 keyholder_state[map] = {}
165
166 keyholder_state[map].erase(kh_path)
167 letters_in_keyholders.erase(key)
168
169 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(key, 0)
170
171 update_unlocks()
172 save()
173
174
175func block_letter(key):
176 if not letters_blocked.has(key):
177 letters_blocked.append(key)
178
179 update_unlocks()
180
181
182func load_keyholders(map):
183 if keyholder_state.has(map):
184 var khs = keyholder_state[map]
185
186 for path in khs.keys():
187 var key = khs[path]
188 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
189 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
190 )
191
192
193func reset_keyholders():
194 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty()
195
196 if keyholder_state.has(global.map):
197 for path in keyholder_state[global.map]:
198 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
199 keyholder_state[global.map][path], 0
200 )
201
202 keyholder_state.clear()
203 letters_in_keyholders.clear()
204 letters_blocked.clear()
205
206 update_unlocks()
207 save()
208
209 return cleared_anything
210
211
212func remote_keyboard_updated(updates):
213 var reverse = {}
214 var should_update = false
215
216 for k in updates:
217 if not letters_saved.has(k) or updates[k] > letters_saved[k]:
218 letters_saved[k] = updates[k]
219 should_update = true
220 elif updates[k] < letters_saved[k]:
221 reverse[k] = letters_saved[k]
222
223 if should_update:
224 update_unlocks()
225
226 if not reverse.is_empty():
227 var ap = global.get_node("Archipelago")
228 ap.client.updateKeyboard(reverse)
diff --git a/apworld/client/locationListener.gd b/apworld/client/locationListener.gd new file mode 100644 index 0000000..71792ed --- /dev/null +++ b/apworld/client/locationListener.gd
@@ -0,0 +1,20 @@
1extends Receiver
2
3var location_id
4
5
6func _ready():
7 super._ready()
8
9
10func handleTriggered():
11 triggered += 1
12 if triggered >= total:
13 var ap = global.get_node("Archipelago")
14 ap.send_location(location_id)
15
16
17func handleUntriggered():
18 triggered -= 1
19 if triggered < total:
20 pass
diff --git a/apworld/client/main.gd b/apworld/client/main.gd new file mode 100644 index 0000000..8cac24c --- /dev/null +++ b/apworld/client/main.gd
@@ -0,0 +1,314 @@
1extends Node
2
3
4func _ready():
5 var runtime = global.get_node("Runtime")
6
7 # Some helpful logging.
8 if Steam.isSubscribed():
9 global._print("Provisioning successful! Build ID: %d" % Steam.getAppBuildId())
10 else:
11 global._print("Provisioning failed.")
12
13 # Undo the load screen removing our cursor
14 get_tree().get_root().set_disable_input(false)
15 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
16
17 # Increase the WebSocket input buffer size so that we can download large
18 # data packages.
19 ProjectSettings.set_setting("network/limits/websocket_client/max_in_buffer_kb", 8192)
20
21 switcher.layer = 4
22
23 # Create the global AP manager, if it doesn't already exist.
24 if not global.has_node("Archipelago"):
25 var ap_script = runtime.load_script("manager.gd")
26 var ap_instance = ap_script.new()
27 ap_instance.name = "Archipelago"
28
29 ap_instance.SCRIPT_client = runtime.load_script("client.gd")
30 ap_instance.SCRIPT_keyboard = runtime.load_script("keyboard.gd")
31 ap_instance.SCRIPT_locationListener = runtime.load_script("locationListener.gd")
32 ap_instance.SCRIPT_minimap = runtime.load_script("minimap.gd")
33 ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd")
34 ap_instance.SCRIPT_websocketserver = runtime.load_script("vendor/WebSocketServer.gd")
35
36 global.add_child(ap_instance)
37
38 # Let's also inject any scripts we need to inject now.
39 installScriptExtension(runtime.load_script("allowNumbers.gd"))
40 installScriptExtension(runtime.load_script("animationListener.gd"))
41 installScriptExtension(runtime.load_script("collectable.gd"))
42 installScriptExtension(runtime.load_script("door.gd"))
43 installScriptExtension(runtime.load_script("keyHolder.gd"))
44 installScriptExtension(runtime.load_script("keyHolderChecker.gd"))
45 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd"))
46 installScriptExtension(runtime.load_script("painting.gd"))
47 installScriptExtension(runtime.load_script("paintingAuto.gd"))
48 installScriptExtension(runtime.load_script("panel.gd"))
49 installScriptExtension(runtime.load_script("pauseMenu.gd"))
50 installScriptExtension(runtime.load_script("player.gd"))
51 installScriptExtension(runtime.load_script("rteMenu.gd"))
52 installScriptExtension(runtime.load_script("saver.gd"))
53 installScriptExtension(runtime.load_script("teleport.gd"))
54 installScriptExtension(runtime.load_script("teleportListener.gd"))
55 installScriptExtension(runtime.load_script("unlockReaderListener.gd"))
56 installScriptExtension(runtime.load_script("visibilityListener.gd"))
57 installScriptExtension(runtime.load_script("worldport.gd"))
58 installScriptExtension(runtime.load_script("worldportListener.gd"))
59
60 var proto_script = runtime.load_script("../generated/proto.gd")
61 var gamedata_script = runtime.load_script("gamedata.gd")
62 var gamedata_instance = gamedata_script.new(proto_script)
63 gamedata_instance.load(runtime.read_path("../generated/data.binpb"))
64 gamedata_instance.name = "Gamedata"
65 global.add_child(gamedata_instance)
66
67 var messages_script = runtime.load_script("messages.gd")
68 var messages_instance = messages_script.new()
69 messages_instance.name = "Messages"
70 messages_instance.SCRIPT_rainbowText = runtime.load_script("rainbowText.gd")
71 global.add_child(messages_instance)
72
73 var effects_script = runtime.load_script("effects.gd")
74 var effects_instance = effects_script.new()
75 effects_instance.name = "Effects"
76 global.add_child(effects_instance)
77
78 var textclient_script = runtime.load_script("textclient.gd")
79 var textclient_instance = textclient_script.new()
80 textclient_instance.name = "Textclient"
81 global.add_child(textclient_instance)
82
83 var compass_overlay_script = runtime.load_script("compass_overlay.gd")
84 var compass_overlay_instance = compass_overlay_script.new()
85 compass_overlay_instance.name = "Compass"
86 compass_overlay_instance.SCRIPT_compass = runtime.load_script("compass.gd")
87 global.add_child(compass_overlay_instance)
88
89 unlocks.data["advanced_mastery"] = ""
90 unlocks.data["charismatic_mastery"] = ""
91 unlocks.data["crystalline_mastery"] = ""
92 unlocks.data["fuzzy_mastery"] = ""
93 unlocks.data["icarus_mastery"] = ""
94 unlocks.data["stellar_mastery"] = ""
95
96 var ap = global.get_node("Archipelago")
97 var gamedata = global.get_node("Gamedata")
98 ap.ap_connected.connect(connectionSuccessful)
99 ap.could_not_connect.connect(connectionUnsuccessful)
100 ap.connect_status.connect(connectionStatus)
101
102 # Populate textboxes with AP settings.
103 get_node("../Panel/server_box").text = ap.ap_server
104 get_node("../Panel/player_box").text = ap.ap_user
105 get_node("../Panel/password_box").text = ap.ap_pass
106
107 var history_box = get_node("../Panel/connection_history")
108 if ap.connection_history.is_empty():
109 history_box.disabled = true
110 else:
111 history_box.disabled = false
112
113 var i = 0
114 for details in ap.connection_history:
115 history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i)
116 i += 1
117
118 history_box.get_popup().id_pressed.connect(historySelected)
119
120 # Show client version.
121 var version = gamedata.objects.get_version()
122 get_node("../Panel/title").text = (
123 "ARCHIPELAGO (%d.%d.%d)" % [version.get_major(), version.get_minor(), version.get_patch()]
124 )
125
126 # Increase font size in text boxes.
127 get_node("../Panel/server_box").add_theme_font_size_override("font_size", 36)
128 get_node("../Panel/player_box").add_theme_font_size_override("font_size", 36)
129 get_node("../Panel/password_box").add_theme_font_size_override("font_size", 36)
130
131 # Set up version mismatch dialog.
132 get_node("../Panel/VersionMismatch").confirmed.connect(startGame)
133 get_node("../Panel/VersionMismatch").get_cancel_button().pressed.connect(
134 versionMismatchDeclined
135 )
136
137 # Set up buttons.
138 get_node("../Panel/connect_button").pressed.connect(_connect_pressed)
139 get_node("../Panel/quit_button").pressed.connect(_back_pressed)
140
141
142func _connect_pressed():
143 get_node("../Panel/connect_button").disabled = true
144
145 var ap = global.get_node("Archipelago")
146 ap.ap_server = get_node("../Panel/server_box").text
147 ap.ap_user = get_node("../Panel/player_box").text
148 ap.ap_pass = get_node("../Panel/password_box").text
149 ap.saveSettings()
150
151 ap.connectToServer()
152
153
154func _back_pressed():
155 var ap = global.get_node("Archipelago")
156 ap.disconnect_from_ap()
157 ap.client.sendQuit()
158
159 get_tree().quit()
160
161
162# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
163func installScriptExtension(childScript: Resource):
164 # Force Godot to compile the script now.
165 # We need to do this here to ensure that the inheritance chain is
166 # properly set up, and multiple mods can chain-extend the same
167 # class multiple times.
168 # This is also needed to make Godot instantiate the extended class
169 # when creating singletons.
170 # The actual instance is thrown away.
171 childScript.new()
172
173 var parentScript = childScript.get_base_script()
174 var parentScriptPath = parentScript.resource_path
175 global._print("ModLoader: Installing script extension over %s" % parentScriptPath)
176 childScript.take_over_path(parentScriptPath)
177
178
179func connectionStatus(message):
180 var popup = get_node("../Panel/AcceptDialog")
181 popup.title = "Connecting to Archipelago"
182 popup.dialog_text = message
183 popup.exclusive = true
184 popup.get_ok_button().visible = false
185 popup.popup_centered()
186
187
188func connectionSuccessful():
189 var ap = global.get_node("Archipelago")
190 var gamedata = global.get_node("Gamedata")
191
192 # Check for major version mismatch.
193 if ap.apworld_version[0] != gamedata.objects.get_version().get_major():
194 get_node("../Panel/AcceptDialog").exclusive = false
195
196 var popup = get_node("../Panel/VersionMismatch")
197 popup.title = "Version Mismatch!"
198 popup.dialog_text = (
199 "This slot was generated using v%d.%d.%d of the Lingo 2 apworld,\nwhich has a different major version than this client (v%d.%d.%d).\nIt is highly recommended to play using the correct version of the client.\nYou may experience bugs or logic issues if you continue."
200 % [
201 ap.apworld_version[0],
202 ap.apworld_version[1],
203 ap.apworld_version[2],
204 gamedata.objects.get_version().get_major(),
205 gamedata.objects.get_version().get_minor(),
206 gamedata.objects.get_version().get_patch()
207 ]
208 )
209 popup.exclusive = true
210 popup.popup_centered()
211
212 return
213
214 startGame()
215
216
217func startGame():
218 var ap = global.get_node("Archipelago")
219
220 # Save connection details
221 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
222 if ap.connection_history.has(connection_details):
223 ap.connection_history.erase(connection_details)
224 ap.connection_history.push_front(connection_details)
225 if ap.connection_history.size() > 10:
226 ap.connection_history.resize(10)
227 ap.saveSettings()
228
229 # Switch to the_entry
230 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
231 global.user = ap.getSaveFileName()
232 global.universe = "lingo"
233
234 if ap.daedalus_only:
235 global.map = "daedalus"
236 else:
237 global.map = "the_entry"
238
239 unlocks.resetCollectables()
240 unlocks.resetData()
241 unlocks.loadCollectables()
242 unlocks.loadData()
243
244 ap.setup_keys()
245
246 unlocks.unlockKey("capslock", 1)
247
248 if ap.shuffle_worldports:
249 settings.worldport_fades = "default"
250 else:
251 settings.worldport_fades = "never"
252
253 clearResourceCache("res://objects/meshes/gridDoor.tscn")
254 clearResourceCache("res://objects/nodes/allowNumbers.tscn")
255 clearResourceCache("res://objects/nodes/collectable.tscn")
256 clearResourceCache("res://objects/nodes/door.tscn")
257 clearResourceCache("res://objects/nodes/keyHolder.tscn")
258 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn")
259 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
260 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
261 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
262 clearResourceCache("res://objects/nodes/listeners/unlockReaderListener.tscn")
263 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn")
264 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
265 clearResourceCache("res://objects/nodes/panel.tscn")
266 clearResourceCache("res://objects/nodes/player.tscn")
267 clearResourceCache("res://objects/nodes/saver.tscn")
268 clearResourceCache("res://objects/nodes/teleport.tscn")
269 clearResourceCache("res://objects/nodes/worldport.tscn")
270 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn")
271 clearResourceCache("res://objects/scenes/menus/rte_inner.tscn")
272
273 var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
274 if paintings_dir:
275 paintings_dir.list_dir_begin()
276 var file_name = paintings_dir.get_next()
277 while file_name != "":
278 if not paintings_dir.current_is_dir() and file_name.ends_with(".tscn"):
279 clearResourceCache("res://objects/meshes/paintings/" + file_name)
280 file_name = paintings_dir.get_next()
281
282 switcher.switch_map.call_deferred("res://objects/scenes/%s.tscn" % global.map)
283
284
285func connectionUnsuccessful(error_message):
286 get_node("../Panel/connect_button").disabled = false
287
288 var popup = get_node("../Panel/AcceptDialog")
289 popup.title = "Could not connect to Archipelago"
290 popup.dialog_text = error_message
291 popup.exclusive = true
292 popup.get_ok_button().visible = true
293 popup.popup_centered()
294
295
296func versionMismatchDeclined():
297 get_node("../Panel/AcceptDialog").hide()
298 get_node("../Panel/connect_button").disabled = false
299
300 var ap = global.get_node("Archipelago")
301 ap.disconnect_from_ap()
302
303
304func historySelected(index):
305 var ap = global.get_node("Archipelago")
306 var details = ap.connection_history[index]
307
308 get_node("../Panel/server_box").text = details[0]
309 get_node("../Panel/player_box").text = details[1]
310 get_node("../Panel/password_box").text = details[2]
311
312
313func clearResourceCache(path):
314 ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd new file mode 100644 index 0000000..9b790c0 --- /dev/null +++ b/apworld/client/manager.gd
@@ -0,0 +1,778 @@
1extends Node
2
3var SCRIPT_client
4var SCRIPT_keyboard
5var SCRIPT_locationListener
6var SCRIPT_minimap
7var SCRIPT_victoryListener
8var SCRIPT_websocketserver
9
10var ap_server = ""
11var ap_user = ""
12var ap_pass = ""
13var connection_history = []
14var show_compass = false
15var show_locations = false
16var show_minimap = false
17var prioritize_current_map = false
18
19var client
20var keyboard
21
22var _localdata_file = ""
23var _last_new_item = -1
24var _batch_locations = false
25var _held_locations = []
26var _held_location_scouts = []
27var _location_scouts = {}
28var _item_locks = {}
29var _inverse_item_locks = {}
30var _held_letters = {}
31var _letters_setup = false
32var _already_connected = false
33var _ignored_locations = []
34var _map_scripts = {}
35
36const kSHUFFLE_LETTERS_VANILLA = 0
37const kSHUFFLE_LETTERS_UNLOCKED = 1
38const kSHUFFLE_LETTERS_PROGRESSIVE = 2
39const kSHUFFLE_LETTERS_VANILLA_CYAN = 3
40const kSHUFFLE_LETTERS_ITEM_CYAN = 4
41
42const kLETTER_BEHAVIOR_VANILLA = 0
43const kLETTER_BEHAVIOR_ITEM = 1
44const kLETTER_BEHAVIOR_UNLOCKED = 2
45
46const kCYAN_DOOR_BEHAVIOR_H2 = 0
47const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
48const kCYAN_DOOR_BEHAVIOR_ITEM = 2
49
50const kFAST_TRAVEL_ACCESS_VANILLA = 0
51const kFAST_TRAVEL_ACCESS_UNLOCKED = 1
52const kFAST_TRAVEL_ACCESS_ITEMS = 2
53
54const kEndingNameByVictoryValue = {
55 0: "GRAY",
56 1: "PURPLE",
57 2: "MINT",
58 3: "BLACK",
59 4: "BLUE",
60 5: "CYAN",
61 6: "RED",
62 7: "PLUM",
63 8: "ORANGE",
64 9: "GOLD",
65 10: "YELLOW",
66 11: "GREEN",
67 12: "WHITE",
68}
69
70var apworld_version = [0, 0, 0]
71var custom_mint_ending = ""
72var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
73var daedalus_only = false
74var daedalus_roof_access = false
75var enable_gift_maps = []
76var enable_icarus = false
77var endings_requirement = 0
78var fast_travel_access = 0
79var keyholder_sanity = false
80var masteries_requirement = 0
81var music_mapping = {}
82var port_pairings = {}
83var rte_mapping = []
84var shuffle_control_center_colors = false
85var shuffle_doors = false
86var shuffle_gallery_paintings = false
87var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
88var shuffle_symbols = false
89var shuffle_worldports = false
90var slot_rng = null
91var strict_cyan_ending = false
92var strict_purple_ending = false
93var victory_condition = -1
94
95var color_by_material_path = {}
96
97signal could_not_connect
98signal connect_status
99signal ap_connected
100
101
102func _init():
103 # Read AP settings from file, if there are any
104 if FileAccess.file_exists("user://ap_settings"):
105 var file = FileAccess.open("user://ap_settings", FileAccess.READ)
106 var data = file.get_var(true)
107 file.close()
108
109 if typeof(data) != TYPE_ARRAY:
110 global._print("AP settings file is corrupted")
111 data = []
112
113 if data.size() > 0:
114 ap_server = data[0]
115
116 if data.size() > 1:
117 ap_user = data[1]
118
119 if data.size() > 2:
120 ap_pass = data[2]
121
122 if data.size() > 3:
123 connection_history = data[3]
124
125 if data.size() > 4:
126 show_compass = data[4]
127
128 if data.size() > 5:
129 show_locations = data[5]
130
131 if data.size() > 6:
132 show_minimap = data[6]
133
134 if data.size() > 7:
135 prioritize_current_map = data[7]
136
137 # We need to create a mapping from material paths to the original colors of
138 # those materials. We force reload the materials, overwriting any custom
139 # textures, and create the mapping. We then reload the textures in case the
140 # player had a custom one enabled.
141 var directory = DirAccess.open("res://assets/materials")
142 for material_name in directory.get_files():
143 var material = ResourceLoader.load(
144 "res://assets/materials/" + material_name, "", ResourceLoader.CACHE_MODE_REPLACE
145 )
146
147 color_by_material_path[material.resource_path] = Color(material.albedo_color)
148
149 settings.load_user_textures()
150
151
152func _ready():
153 client = SCRIPT_client.new()
154 client.SCRIPT_websocketserver = SCRIPT_websocketserver
155
156 client.item_received.connect(_process_item)
157 client.location_scout_received.connect(_process_location_scout)
158 client.text_message_received.connect(_process_text_message)
159 client.item_sent_notification.connect(_process_item_sent_notification)
160 client.hint_received.connect(_process_hint_received)
161 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
162 client.checked_locations_updated.connect(_on_checked_locations_updated)
163 client.ignored_locations_updated.connect(_on_ignored_locations_updated)
164 client.hinted_locations_updated.connect(_on_hinted_locations_updated)
165 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
166 client.door_latched.connect(_on_door_latched)
167
168 client.could_not_connect.connect(_client_could_not_connect)
169 client.connect_status.connect(_client_connect_status)
170 client.client_connected.connect(_client_connected)
171
172 add_child(client)
173
174 keyboard = SCRIPT_keyboard.new()
175 add_child(keyboard)
176 client.keyboard_update_received.connect(keyboard.remote_keyboard_updated)
177
178
179func saveSettings():
180 # Save the AP settings to disk.
181 var path = "user://ap_settings"
182 var file = FileAccess.open(path, FileAccess.WRITE)
183
184 var data = [
185 ap_server,
186 ap_user,
187 ap_pass,
188 connection_history,
189 show_compass,
190 show_locations,
191 show_minimap,
192 prioritize_current_map,
193 ]
194 file.store_var(data, true)
195 file.close()
196
197
198func saveLocaldata():
199 # Save the MW/slot specific settings to disk.
200 var dir = DirAccess.open("user://")
201 var folder = "archipelago_data"
202 if not dir.dir_exists(folder):
203 dir.make_dir(folder)
204
205 var file = FileAccess.open(_localdata_file, FileAccess.WRITE)
206
207 var data = [
208 _last_new_item,
209 ]
210 file.store_var(data, true)
211 file.close()
212
213
214func connectToServer():
215 _last_new_item = -1
216 _batch_locations = false
217 _held_locations = []
218 _held_location_scouts = []
219 _location_scouts = {}
220 _letters_setup = false
221 _held_letters = {}
222 _already_connected = false
223
224 client.connectToServer(ap_server, ap_user, ap_pass)
225
226
227func getSaveFileName():
228 return "zzAP_%s_%d" % [client._seed, client._slot]
229
230
231func disconnect_from_ap():
232 _already_connected = false
233
234 var effects = global.get_node("Effects")
235 effects.set_connection_lost(false)
236
237 client.disconnect_from_ap()
238
239
240func get_item_id_for_door(door_id):
241 return _item_locks.get(door_id, null)
242
243
244func _process_item(item, amount):
245 var gamedata = global.get_node("Gamedata")
246
247 var item_id = int(item["id"])
248 var prog_id = null
249 if _inverse_item_locks.has(item_id):
250 for lock in _inverse_item_locks.get(item_id):
251 if lock[1] != amount:
252 continue
253
254 if gamedata.progressive_id_by_ap_id.has(item_id):
255 prog_id = lock[0]
256
257 if gamedata.get_door_map_name(lock[0]) != global.map:
258 continue
259
260 # TODO: fix doors opening from door groups
261 var receivers = gamedata.get_door_receivers(lock[0])
262 var scene = get_tree().get_root().get_node_or_null("scene")
263 if scene != null:
264 for receiver in receivers:
265 var rnode = scene.get_node_or_null(receiver)
266 if rnode != null:
267 rnode.handleTriggered()
268
269 var letter_id = gamedata.letter_id_by_ap_id.get(item_id, null)
270 if letter_id != null:
271 var letter = gamedata.objects.get_letters()[letter_id]
272 if not letter.has_level2() or not letter.get_level2():
273 _process_key_item(letter.get_key(), amount)
274
275 if gamedata.symbol_item_ids.has(item_id):
276 var player = get_tree().get_root().get_node_or_null("scene/player")
277 if player != null:
278 player.evaluate_solvability.emit()
279
280 if item_id == gamedata.objects.get_special_ids()["A Job Well Done"]:
281 update_job_well_done_sign()
282
283 if item_id == gamedata.objects.get_special_ids()["Numbers"] and global.map == "the_fuzzy":
284 global.allow_numbers = true
285
286 if gamedata.map_id_by_rte_ap_id.has(item_id):
287 var rteInner = get_tree().get_root().get_node_or_null(
288 "scene/player/pause_menu/menu/return/rteInner"
289 )
290 if rteInner != null:
291 rteInner.refreshButtons()
292
293 # Show a message about the item if it's new.
294 if int(item["index"]) > _last_new_item:
295 _last_new_item = int(item["index"])
296 saveLocaldata()
297
298 var full_item_name = item["text"]
299 if prog_id != null:
300 var door = gamedata.objects.get_doors()[prog_id]
301 full_item_name = "%s (%s)" % [full_item_name, door.get_name()]
302
303 var message
304 if "sender" in item:
305 message = (
306 "Received %s from %s"
307 % [wrapInItemColorTags(full_item_name, item["flags"]), item["sender"]]
308 )
309 else:
310 message = "Found %s" % wrapInItemColorTags(full_item_name, item["flags"])
311
312 if gamedata.anti_trap_ids.has(item):
313 keyboard.block_letter(gamedata.anti_trap_ids[item])
314
315 global._print(message)
316
317 global.get_node("Messages").showMessage(message)
318
319
320func _process_item_sent_notification(message):
321 var sentMsg = (
322 "Sent %s to %s"
323 % [
324 wrapInItemColorTags(message["item_name"], message["item_flags"]),
325 message["receiver_name"]
326 ]
327 )
328 #if _hinted_locations.has(message["item"]["location"]):
329 # sentMsg += " ([color=#fafad2]Hinted![/color])"
330 global.get_node("Messages").showMessage(sentMsg)
331
332
333func _process_hint_received(message):
334 var is_for = ""
335 if message["self"] == 0:
336 is_for = " for %s" % message["receiver_name"]
337
338 global.get_node("Messages").showMessage(
339 (
340 "Hint: %s%s is on %s"
341 % [
342 wrapInItemColorTags(message["item_name"], message["item_flags"]),
343 is_for,
344 message["location_name"]
345 ]
346 )
347 )
348
349
350func _process_text_message(message):
351 var parts = []
352 for message_part in message:
353 if message_part["type"] == "text":
354 parts.append(message_part["text"])
355 elif message_part["type"] == "player":
356 if message_part["self"] == 1:
357 parts.append("[color=#ee00ee]%s[/color]" % message_part["text"])
358 else:
359 parts.append("[color=#fafad2]%s[/color]" % message_part["text"])
360 elif message_part["type"] == "item":
361 parts.append(wrapInItemColorTags(message_part["text"], int(message_part["flags"])))
362 elif message_part["type"] == "location":
363 parts.append("[color=#00ff7f]%s[/color]" % message_part["text"])
364
365 var textclient_node = global.get_node("Textclient")
366 if textclient_node != null:
367 textclient_node.parse_printjson("".join(parts))
368
369
370func _process_location_scout(location_id, item_name, player_name, flags, for_self):
371 _location_scouts[location_id] = {
372 "item": item_name, "player": player_name, "flags": flags, "for_self": for_self
373 }
374
375 if for_self and flags & 4 != 0:
376 # This is a trap for us, so let's not display it.
377 return
378
379 var gamedata = global.get_node("Gamedata")
380 var map_id = gamedata.map_id_by_name.get(global.map)
381
382 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
383 if letter_id != null:
384 var letter = gamedata.objects.get_letters()[letter_id]
385 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
386 if room.get_map_id() == map_id:
387 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
388 letter.get_path()
389 )
390 if collectable != null:
391 collectable.setScoutedText(item_name)
392
393
394func _on_accessible_locations_updated():
395 var textclient_node = global.get_node("Textclient")
396 if textclient_node != null:
397 textclient_node.update_locations()
398
399
400func _on_checked_locations_updated():
401 var textclient_node = global.get_node("Textclient")
402 if textclient_node != null:
403 textclient_node.update_locations(false)
404
405
406func _on_checked_worldports_updated():
407 var textclient_node = global.get_node("Textclient")
408 if textclient_node != null:
409 textclient_node.update_locations()
410 textclient_node.update_worldports()
411
412
413func _on_ignored_locations_updated(locations):
414 _ignored_locations = locations
415
416 var textclient_node = global.get_node("Textclient")
417 if textclient_node != null:
418 textclient_node.update_locations()
419
420
421func _on_hinted_locations_updated():
422 var textclient_node = global.get_node("Textclient")
423 if textclient_node != null:
424 textclient_node.update_locations()
425
426
427func _on_door_latched(door_id):
428 var gamedata = global.get_node("Gamedata")
429 if gamedata.get_door_map_name(door_id) != global.map:
430 return
431
432 var receivers = gamedata.get_door_receivers(door_id)
433 var scene = get_tree().get_root().get_node_or_null("scene")
434 if scene != null:
435 for receiver in receivers:
436 var rnode = scene.get_node_or_null(receiver)
437 if rnode != null:
438 rnode.handleTriggered()
439
440
441func _client_could_not_connect(message):
442 could_not_connect.emit(message)
443
444 if global.loaded:
445 var effects = global.get_node("Effects")
446 effects.set_connection_lost(true)
447
448 var messages = global.get_node("Messages")
449 messages.showMessage("Connection to multiworld lost.")
450
451
452func _client_connect_status(message):
453 connect_status.emit(message)
454
455
456func _client_connected(slot_data):
457 var effects = global.get_node("Effects")
458 effects.set_connection_lost(false)
459
460 if _already_connected:
461 var messages = global.get_node("Messages")
462 messages.showMessage("Reconnected to multiworld!")
463 return
464
465 _already_connected = true
466
467 var gamedata = global.get_node("Gamedata")
468
469 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
470 _last_new_item = -1
471
472 if FileAccess.file_exists(_localdata_file):
473 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
474 var localdata = []
475 if ap_file != null:
476 localdata = ap_file.get_var(true)
477 ap_file.close()
478
479 if typeof(localdata) != TYPE_ARRAY:
480 print("AP localdata file is corrupted")
481 localdata = []
482
483 if localdata.size() > 0:
484 _last_new_item = localdata[0]
485
486 # Read slot data.
487 custom_mint_ending = slot_data.get("custom_mint_ending", "")
488 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
489 daedalus_only = bool(slot_data.get("daedalus_only", false))
490 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
491 enable_gift_maps = slot_data.get("enable_gift_maps", [])
492 enable_icarus = bool(slot_data.get("enable_icarus", false))
493 endings_requirement = int(slot_data.get("endings_requirement", 0))
494 fast_travel_access = int(slot_data.get("fast_travel_access", 0))
495 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
496 masteries_requirement = int(slot_data.get("masteries_requirement", 0))
497 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
498 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
499 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
500 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
501 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
502 shuffle_worldports = bool(slot_data.get("shuffle_worldports", false))
503 strict_cyan_ending = bool(slot_data.get("strict_cyan_ending", false))
504 strict_purple_ending = bool(slot_data.get("strict_purple_ending", false))
505 victory_condition = int(slot_data.get("victory_condition", 0))
506
507 if slot_data.has("version"):
508 var version_msg = slot_data["version"]
509 apworld_version = [int(version_msg[0]), int(version_msg[1]), 0]
510 if version_msg.size() > 2:
511 apworld_version[2] = int(version_msg[2])
512
513 port_pairings.clear()
514 if slot_data.has("port_pairings"):
515 var raw_pp = slot_data.get("port_pairings")
516
517 for p1 in raw_pp.keys():
518 port_pairings[gamedata.port_id_by_ap_id[int(p1)]] = gamedata.port_id_by_ap_id[int(
519 raw_pp[p1]
520 )]
521
522 rte_mapping.clear()
523 if slot_data.has("rte"):
524 rte_mapping = slot_data.get("rte")
525
526 slot_rng = RandomNumberGenerator.new()
527 slot_rng.seed = int(slot_data.get("seed", 0))
528
529 music_mapping.clear()
530 if bool(slot_data.get("shuffle_music", false)):
531 for map_name in global.reserved_scenes:
532 var track_index = slot_rng.randi_range(0, musicPlayer.all_tracks.size() - 1)
533 music_mapping[map_name] = musicPlayer.all_tracks.keys()[track_index]
534
535 # Set up item locks.
536 _item_locks = {}
537
538 if shuffle_doors or daedalus_only:
539 for door in gamedata.objects.get_doors():
540 if (
541 door.get_type() != gamedata.SCRIPT_proto.DoorType.STANDARD
542 and door.get_type() != gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
543 ):
544 continue
545
546 if (
547 not shuffle_doors
548 and not (
549 daedalus_only
550 and door.has_daedalus_only_always_item()
551 and door.get_daedalus_only_always_item()
552 )
553 ):
554 continue
555
556 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
557
558 if shuffle_doors:
559 for progressive in gamedata.objects.get_progressives():
560 for i in range(0, progressive.get_doors().size()):
561 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
562 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
563
564 for door_group in gamedata.objects.get_door_groups():
565 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR:
566 if shuffle_worldports:
567 continue
568 elif door_group.get_type() != gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP:
569 continue
570
571 if (
572 not shuffle_doors
573 and not (
574 daedalus_only
575 and door_group.has_daedalus_only_always_item()
576 and door_group.get_daedalus_only_always_item()
577 )
578 ):
579 continue
580
581 for door in door_group.get_doors():
582 _item_locks[door] = [door_group.get_ap_id(), 1]
583
584 if shuffle_control_center_colors:
585 for door in gamedata.objects.get_doors():
586 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
587 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
588
589 for door_group in gamedata.objects.get_door_groups():
590 if (
591 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR
592 and not shuffle_worldports
593 ):
594 for door in door_group.get_doors():
595 _item_locks[door] = [door_group.get_ap_id(), 1]
596
597 if shuffle_gallery_paintings:
598 for door in gamedata.objects.get_doors():
599 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
600 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
601
602 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
603 for door_group in gamedata.objects.get_door_groups():
604 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
605 for door in door_group.get_doors():
606 if not _item_locks.has(door):
607 _item_locks[door] = [door_group.get_ap_id(), 1]
608
609 # Create a reverse item locks map for processing items.
610 _inverse_item_locks = {}
611
612 for door_id in _item_locks.keys():
613 var lock = _item_locks.get(door_id)
614
615 if not _inverse_item_locks.has(lock[0]):
616 _inverse_item_locks[lock[0]] = []
617
618 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
619
620 if shuffle_worldports:
621 var textclient = global.get_node("Textclient")
622 textclient.setup_worldports()
623
624 ap_connected.emit()
625
626
627func start_batching_locations():
628 _batch_locations = true
629
630
631func send_location(loc_id):
632 if client._checked_locations.has(loc_id):
633 return
634
635 if _batch_locations:
636 _held_locations.append(loc_id)
637 else:
638 client.sendLocation(loc_id)
639
640
641func scout_location(loc_id):
642 if _location_scouts.has(loc_id):
643 return _location_scouts.get(loc_id)
644
645 if _batch_locations:
646 _held_location_scouts.append(loc_id)
647 else:
648 client.scoutLocation(loc_id)
649
650 return null
651
652
653func stop_batching_locations():
654 _batch_locations = false
655
656 if not _held_locations.is_empty():
657 client.sendLocations(_held_locations)
658 _held_locations.clear()
659
660 if not _held_location_scouts.is_empty():
661 client.scoutLocations(_held_location_scouts)
662 _held_location_scouts.clear()
663
664
665func colorForItemType(flags):
666 var int_flags = int(flags)
667 if int_flags & 1: # progression
668 if int_flags & 2: # proguseful
669 return "#f0d200"
670 else:
671 return "#bc51e0"
672 elif int_flags & 2: # useful
673 return "#2b67ff"
674 elif int_flags & 4: # trap
675 return "#d63a22"
676 else: # filler
677 return "#14de9e"
678
679
680func wrapInItemColorTags(text, flags):
681 var int_flags = int(flags)
682 if int_flags & 1 and int_flags & 2: # proguseful
683 return "[rainbow]%s[/rainbow]" % text
684 else:
685 return "[color=%s]%s[/color]" % [colorForItemType(flags), text]
686
687
688func get_letter_behavior(key, level2):
689 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
690 return kLETTER_BEHAVIOR_UNLOCKED
691
692 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
693 if level2:
694 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
695 return kLETTER_BEHAVIOR_VANILLA
696 else:
697 return kLETTER_BEHAVIOR_ITEM
698 else:
699 return kLETTER_BEHAVIOR_UNLOCKED
700
701 if not level2 and ["h", "i", "n", "t"].has(key):
702 # This differs from the equivalent function in the apworld. Logically it is
703 # the same as UNLOCKED since they are in the starting room, but VANILLA
704 # means the player still has to actually pick up the letters.
705 return kLETTER_BEHAVIOR_VANILLA
706
707 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
708 return kLETTER_BEHAVIOR_ITEM
709
710 return kLETTER_BEHAVIOR_VANILLA
711
712
713func setup_keys():
714 keyboard.load_seed()
715
716 _letters_setup = true
717
718 for k in _held_letters.keys():
719 _process_key_item(k, _held_letters[k])
720
721 _held_letters.clear()
722
723
724func _process_key_item(key, level):
725 if not _letters_setup:
726 _held_letters[key] = max(_held_letters.get(key, 0), level)
727 return
728
729 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
730 level += 1
731
732 keyboard.collect_remote_letter(key, level)
733
734
735func update_job_well_done_sign():
736 if global.map != "daedalus":
737 return
738
739 var gamedata = global.get_node("Gamedata")
740 var job_item = gamedata.objects.get_special_ids()["A Job Well Done"]
741 var jobs_done = client.getItemAmount(job_item)
742
743 var sign2 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign2")
744 var sign3 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign3")
745
746 if sign2 != null and sign3 != null:
747 if jobs_done == 0:
748 sign2.text = "what are you doing"
749 sign3.text = "?"
750 elif jobs_done == 1:
751 sign2.text = "a job well done"
752 sign3.text = "is its own reward"
753 else:
754 sign2.text = "%d jobs well done" % jobs_done
755 sign3.text = "are their own reward"
756
757 sign2.get_node("MeshInstance3D").mesh.text = sign2.text
758 sign3.get_node("MeshInstance3D").mesh.text = sign3.text
759
760
761func toggle_ignored_location(loc_id):
762 if loc_id in _ignored_locations:
763 client.removeIgnoredLocation(loc_id)
764 else:
765 client.addIgnoredLocation(loc_id)
766
767
768func get_map_script(map_name):
769 if !_map_scripts.has(map_name):
770 var runtime = global.get_node("Runtime")
771 var script_path = "maps/%s.gd" % map_name
772 if runtime.path_exists(script_path):
773 var script = runtime.load_script(script_path)
774 _map_scripts[map_name] = script.new()
775 else:
776 _map_scripts[map_name] = null
777
778 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..b307984 --- /dev/null +++ b/apworld/client/maps/control_center.gd
@@ -0,0 +1,143 @@
1const kALL_MASTERIES = 19
2
3
4func on_map_load(root):
5 var ap = global.get_node("Archipelago")
6
7 # Remove the door blocking the trophy case.
8 root.get_node("/root/scene/Components/Doors/entry_18").queue_free()
9
10 # Set up mastery listeners for extra maps.
11 _set_up_mastery_listener(root, "advanced")
12 _set_up_mastery_listener(root, "charismatic")
13 _set_up_mastery_listener(root, "crystalline")
14 _set_up_mastery_listener(root, "fuzzy")
15 _set_up_mastery_listener(root, "icarus")
16 _set_up_mastery_listener(root, "stellar")
17
18 if ap.endings_requirement == 0 and ap.masteries_requirement == 0:
19 # Weird edge case. Dunno why I'm even allowing it.
20 var old_door = root.get_node("/root/scene/Components/Doors/entry_19")
21 old_door.queue_free()
22 elif ap.endings_requirement != 12 or ap.masteries_requirement != 0:
23 # Set up listeners for the potential White Ending requirements.
24 var merging_prefab = preload("res://objects/nodes/listeners/mergingListener.tscn")
25
26 var old_door = root.get_node("/root/scene/Components/Doors/entry_19")
27 var new_door = old_door.duplicate()
28 new_door.name = "entry_19_new"
29 new_door.senders.clear()
30 new_door.senderGroup.clear()
31 new_door.excludeSenders.clear()
32
33 if ap.endings_requirement == 12:
34 new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners"))
35 elif ap.endings_requirement > 0:
36 if ap.masteries_requirement == 0:
37 new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners"))
38 new_door.complete_at = ap.endings_requirement
39 else:
40 var endings_merge = merging_prefab.instantiate()
41 endings_merge.name = "EndingsMerge"
42 endings_merge.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners"))
43 endings_merge.complete_at = ap.endings_requirement
44 root.get_node("/root/scene/Components").add_child.call_deferred(endings_merge)
45 new_door.senders.append(NodePath("/root/scene/Components/EndingsMerge"))
46
47 if ap.masteries_requirement == kALL_MASTERIES:
48 new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/MasteryListeners"))
49 new_door.excludeSenders.append(
50 NodePath("/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite")
51 )
52 elif ap.masteries_requirement > 0:
53 if ap.endings_requirement == 0:
54 new_door.senderGroup.append(
55 NodePath("/root/scene/Meshes/Trophies/MasteryListeners")
56 )
57 new_door.excludeSenders.append(
58 NodePath(
59 "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite"
60 )
61 )
62 new_door.complete_at = ap.masteries_requirement
63 else:
64 var masteries_merge = merging_prefab.instantiate()
65 masteries_merge.name = "MasteriesMerge"
66 masteries_merge.senderGroup.append(
67 NodePath("/root/scene/Meshes/Trophies/MasteryListeners")
68 )
69 masteries_merge.excludeSenders.append(
70 NodePath(
71 "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite"
72 )
73 )
74 masteries_merge.complete_at = ap.masteries_requirement
75 root.get_node("/root/scene/Components").add_child.call_deferred(masteries_merge)
76 new_door.senders.append(NodePath("/root/scene/Components/MasteriesMerge"))
77
78 old_door.queue_free()
79 root.get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door)
80
81 # Display White Ending requirements.
82 var ending_count = 0
83 var mastery_count = 0
84 for key in unlocks.data:
85 if unlocks.data[key] == "unlocked":
86 if key.ends_with("_ending") and key != "free_ending":
87 ending_count += 1
88 elif key.ends_with("_mastery"):
89 mastery_count += 1
90
91 var sign_prefab = preload("res://objects/nodes/sign.tscn")
92 var sign1 = sign_prefab.instantiate()
93 sign1.position = Vector3(87.5, 5, -42.01)
94 sign1.text = "Endings: %d/%d" % [ending_count, ap.endings_requirement]
95 root.get_node("/root/scene").add_child.call_deferred(sign1)
96
97 var sign2 = sign_prefab.instantiate()
98 sign2.position = Vector3(87.5, 5, -15.99)
99 sign2.rotation_degrees.y = 180
100 sign2.text = "Masteries: %d/%d" % [mastery_count, ap.masteries_requirement]
101 root.get_node("/root/scene").add_child.call_deferred(sign2)
102
103 # Handle custom Mint Ending.
104 if ap.custom_mint_ending != "":
105 var panel_prefab = preload("res://objects/nodes/panel.tscn")
106 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
107
108 var mint_ending = root.get_node("/root/scene/Components/Endings/mint_ending")
109
110 var mint_panel = panel_prefab.instantiate()
111 mint_panel.name = "mint_panel"
112 mint_panel.clue = ap.custom_mint_ending
113 mint_panel.symbol = ""
114 mint_panel.answer = ap.custom_mint_ending
115 mint_panel.position = Vector3(-63, 3, -29)
116 mint_panel.rotation_degrees = Vector3(-45, 90, 0)
117 root.get_node("/root/scene").add_child.call_deferred(mint_panel)
118
119 var mint_tpl = tpl_prefab.instantiate()
120 mint_tpl.name = "mint_tpl"
121 mint_tpl.teleport_point = mint_ending.position
122 mint_tpl.teleport_rotate = mint_ending.rotation_degrees
123 mint_tpl.target_path = mint_ending
124 mint_tpl.senders.append(NodePath("/root/scene/mint_panel"))
125 root.get_node("/root/scene").add_child.call_deferred(mint_tpl)
126
127 var mint_tpl2 = tpl_prefab.instantiate()
128 mint_tpl2.name = "mint_tpl2"
129 mint_tpl2.teleport_point = Vector3(0, -1000, 0)
130 mint_tpl2.target_path = mint_panel
131 mint_tpl2.senders.append(NodePath("/root/scene/mint_panel"))
132 root.get_node("/root/scene").add_child.call_deferred(mint_tpl2)
133
134 mint_ending.position.y = -1000
135
136
137func _set_up_mastery_listener(root, name):
138 var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn")
139 var url = prefab.instantiate()
140 url.name = "unlockReaderListenerMastery_%s" % name
141 url.key = "%s_mastery" % name
142 url.value = "unlocked"
143 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_gallery.gd b/apworld/client/maps/the_gallery.gd new file mode 100644 index 0000000..6731c16 --- /dev/null +++ b/apworld/client/maps/the_gallery.gd
@@ -0,0 +1,7 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 if ap.daedalus_only:
5 # Prevent QUESTION door from opening on Daedalus Only mode.
6 var door = root.get_node("/root/scene/Components/Doors/entry_2")
7 door.animate_distance_y = 0
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/messages.gd b/apworld/client/messages.gd new file mode 100644 index 0000000..ab4f071 --- /dev/null +++ b/apworld/client/messages.gd
@@ -0,0 +1,74 @@
1extends CanvasLayer
2
3var SCRIPT_rainbowText
4
5var _message_queue = []
6var _font
7var _container
8var _ordered_labels = []
9
10
11func _ready():
12 _container = VBoxContainer.new()
13 _container.set_name("Container")
14 _container.anchor_bottom = 1
15 _container.offset_left = 20.0
16 _container.offset_right = 1920.0
17 _container.offset_top = 0.0
18 _container.offset_bottom = -20.0
19 _container.alignment = BoxContainer.ALIGNMENT_END
20 _container.mouse_filter = Control.MOUSE_FILTER_IGNORE
21 self.add_child(_container)
22
23 _font = load("res://assets/fonts/Lingo2.ttf")
24
25
26func _add_message(text):
27 var new_label = RichTextLabel.new()
28 new_label.install_effect(SCRIPT_rainbowText.new())
29 new_label.push_font(_font)
30 new_label.push_font_size(36)
31 new_label.push_outline_color(Color(0, 0, 0, 1))
32 new_label.push_outline_size(2)
33 new_label.append_text(text)
34 new_label.fit_content = true
35
36 _container.add_child(new_label)
37 _ordered_labels.push_back(new_label)
38
39
40func showMessage(text):
41 if _ordered_labels.size() >= 9:
42 _message_queue.append(text)
43 return
44
45 _add_message(text)
46
47 if _ordered_labels.size() > 1:
48 return
49
50 var timeout = 10.0
51 while !_ordered_labels.is_empty():
52 await get_tree().create_timer(timeout).timeout
53
54 if !_ordered_labels.is_empty():
55 var to_remove = _ordered_labels.pop_front()
56 var to_tween = get_tree().create_tween().bind_node(to_remove)
57 to_tween.tween_property(to_remove, "modulate:a", 0.0, 0.5)
58 to_tween.tween_callback(to_remove.queue_free)
59
60 if !_message_queue.is_empty():
61 var next_msg = _message_queue.pop_front()
62 _add_message(next_msg)
63
64 if timeout > 4:
65 timeout -= 3
66
67
68func clear():
69 _message_queue.clear()
70
71 for message_label in _ordered_labels:
72 message_label.queue_free()
73
74 _ordered_labels.clear()
diff --git a/apworld/client/minimap.gd b/apworld/client/minimap.gd new file mode 100644 index 0000000..bf70114 --- /dev/null +++ b/apworld/client/minimap.gd
@@ -0,0 +1,178 @@
1extends CanvasLayer
2
3var player
4var drawer
5var sprite
6var label
7
8var cell_left
9var cell_top
10var cell_right
11var cell_bottom
12var cell_width
13var cell_height
14var center_x_min
15var center_x_max
16var center_y_min
17var center_y_max
18
19
20func _ready():
21 player = get_tree().get_root().get_node("scene/player")
22
23 var svc = PanelContainer.new()
24 svc.anchor_left = 1.0
25 svc.anchor_top = 1.0
26 svc.anchor_right = 1.0
27 svc.anchor_bottom = 1.0
28 svc.offset_left = -320.0
29 svc.offset_top = -320.0
30 svc.offset_right = -64.0
31 svc.offset_bottom = -64.0
32 svc.clip_contents = true
33 add_child(svc)
34
35 var background_color = Color.WHITE
36
37 var world_env = get_tree().get_root().get_node("scene/WorldEnvironment")
38 if world_env != null and world_env.environment != null:
39 if world_env.environment.background_mode == Environment.BG_COLOR:
40 background_color = world_env.environment.background_color
41 elif (
42 world_env.environment.background_mode == Environment.BG_SKY
43 and world_env.environment.sky != null
44 and world_env.environment.sky.sky_material != null
45 ):
46 var sky = world_env.environment.sky.sky_material
47 if sky is PhysicalSkyMaterial:
48 background_color = sky.ground_color
49 elif sky is ProceduralSkyMaterial:
50 background_color = sky.sky_top_color
51
52 var stylebox = StyleBoxFlat.new()
53 stylebox.bg_color = Color(background_color, 0.6)
54 svc.add_theme_stylebox_override("panel", stylebox)
55
56 drawer = Node2D.new()
57 svc.add_child(drawer)
58
59 var gridmap = get_tree().get_root().get_node("scene/GridMap")
60 if gridmap == null:
61 visible = false
62 return
63
64 cell_left = 0
65 cell_top = 0
66 cell_right = 0
67 cell_bottom = 0
68
69 for pos in gridmap.get_used_cells():
70 if pos.x < cell_left:
71 cell_left = pos.x
72 if pos.x > cell_right:
73 cell_right = pos.x
74 if pos.z < cell_top:
75 cell_top = pos.z
76 if pos.z > cell_bottom:
77 cell_bottom = pos.z
78
79 cell_width = cell_right - cell_left + 1
80 cell_height = cell_bottom - cell_top + 1
81
82 var rendered = _renderMap(gridmap)
83
84 var image_texture = ImageTexture.create_from_image(rendered)
85 sprite = Sprite2D.new()
86 sprite.texture = image_texture
87 sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
88 sprite.scale = Vector2(2, 2)
89 sprite.centered = false
90 drawer.add_child(sprite)
91
92 label = Label.new()
93 label.theme = preload("res://assets/themes/baseUI.tres")
94 label.add_theme_font_size_override("font_size", 32)
95 label.text = "@"
96 drawer.add_child(label)
97
98 #var local_tl = gridmap.map_to_local(Vector3i(cell_left, 0, cell_top))
99 #var global_tl = gridmap.to_global(local_tl)
100 #var local_br = gridmap.map_to_local(Vector3i(cell_right, 0, cell_bottom))
101 #var global_br = gridmap.to_global(local_br)
102
103 center_x_min = 0
104 center_x_max = cell_width - 128
105 center_y_min = 0
106 center_y_max = cell_height - 128
107
108 if center_x_max < center_x_min:
109 center_x_min = (center_x_min + center_x_max) / 2
110 center_x_max = center_x_min
111
112 if center_y_max < center_y_min:
113 center_y_min = (center_y_min + center_y_max) / 2
114 center_y_max = center_y_min
115
116
117func _process(_delta):
118 if visible == false:
119 return
120
121 drawer.position.x = clamp(player.position.x - cell_left - 64, center_x_min, center_x_max) * -2
122 drawer.position.y = clamp(player.position.z - cell_top - 64, center_y_min, center_y_max) * -2
123
124 label.position.x = (player.position.x - cell_left) * 2 - 16
125 label.position.y = (player.position.z - cell_top) * 2 - 16
126
127
128func _renderMap(gridmap):
129 var ap = global.get_node("Archipelago")
130 var heights = {}
131
132 var rendered = Image.create_empty(cell_width, cell_height, false, Image.FORMAT_RGBA8)
133 rendered.fill(Color.TRANSPARENT)
134
135 var meshes_node = get_tree().get_root().get_node("scene/Meshes")
136 if meshes_node != null:
137 _renderMeshNode(ap, gridmap, meshes_node, rendered)
138
139 for pos in gridmap.get_used_cells():
140 var in_plane = Vector2i(pos.x, pos.z)
141
142 if in_plane in heights and heights[in_plane] > pos.y:
143 continue
144
145 heights[in_plane] = pos.y
146
147 var cell_item = gridmap.get_cell_item(pos)
148 var mesh = gridmap.mesh_library.get_item_mesh(cell_item)
149 var material = mesh.surface_get_material(0)
150 var color = ap.color_by_material_path.get(material.resource_path, Color.TRANSPARENT)
151
152 rendered.set_pixel(pos.x - cell_left, pos.z - cell_top, color)
153
154 return rendered
155
156
157func _renderMeshNode(ap, gridmap, mesh, rendered):
158 if mesh is MeshInstance3D:
159 var local_tl = gridmap.map_to_local(Vector3i(cell_left, 0, cell_top))
160 var global_tl = gridmap.to_global(local_tl)
161 var mesh_material = mesh.get_surface_override_material(0)
162 if mesh_material != null:
163 var mesh_color = ap.color_by_material_path.get(
164 mesh_material.resource_path, Color.TRANSPARENT
165 )
166
167 for y in range(
168 max(mesh.position.z - mesh.scale.z / 2 - global_tl.z, 0),
169 min(mesh.position.z + mesh.scale.z / 2 - global_tl.z, cell_height)
170 ):
171 for x in range(
172 max(mesh.position.x - mesh.scale.x / 2 - global_tl.x, 0),
173 min(mesh.position.x + mesh.scale.x / 2 - global_tl.x, cell_width)
174 ):
175 rendered.set_pixel(x, y, mesh_color)
176
177 for child in mesh.get_children():
178 _renderMeshNode(ap, gridmap, child, rendered)
diff --git a/apworld/client/painting.gd b/apworld/client/painting.gd new file mode 100644 index 0000000..276d4eb --- /dev/null +++ b/apworld/client/painting.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/painting.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
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
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/panel.gd b/apworld/client/panel.gd new file mode 100644 index 0000000..2cef28e --- /dev/null +++ b/apworld/client/panel.gd
@@ -0,0 +1,101 @@
1extends "res://scripts/nodes/panel.gd"
2
3var panel_logic = null
4var symbol_solvable = true
5
6var black = load("res://assets/materials/black.material")
7
8
9func _ready():
10 super._ready()
11
12 var node_path = String(
13 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
14 )
15
16 var gamedata = global.get_node("Gamedata")
17 var panel_id = gamedata.get_panel_for_map_node_path(global.map, node_path)
18 if panel_id != null:
19 var ap = global.get_node("Archipelago")
20 if ap.shuffle_symbols:
21 if global.map == "the_entry" and node_path == "Panels/Entry/front_1":
22 clue = "i"
23 symbol = ""
24
25 setField("clue", clue)
26 setField("symbol", symbol)
27
28 panel_logic = gamedata.objects.get_panels()[panel_id]
29 checkSymbolSolvable()
30
31 if not symbol_solvable:
32 get_tree().get_root().get_node("scene/player").evaluate_solvability.connect(
33 evaluateSolvability
34 )
35
36
37func checkSymbolSolvable():
38 var old_solvable = symbol_solvable
39 symbol_solvable = true
40
41 if panel_logic == null:
42 # There's no logic for this panel.
43 return
44
45 var ap = global.get_node("Archipelago")
46 if not ap.shuffle_symbols:
47 # Symbols aren't item-locked.
48 return
49
50 var gamedata = global.get_node("Gamedata")
51 for symbol in panel_logic.get_symbols():
52 var item_name = gamedata.kSYMBOL_ITEMS.get(symbol)
53 var item_id = gamedata.objects.get_special_ids()[item_name]
54 if ap.client.getItemAmount(item_id) < 1:
55 symbol_solvable = false
56 break
57
58 if symbol_solvable != old_solvable:
59 if symbol_solvable:
60 setField("clue", clue)
61 setField("symbol", symbol)
62 setField("answer", answer)
63 else:
64 quad_mesh.surface_set_material(0, black)
65 get_node("Hinge/clue").text = "missing"
66 get_node("Hinge/answer").text = "symbols"
67
68
69func checkSolvable(key):
70 checkSymbolSolvable()
71 if not symbol_solvable:
72 return false
73
74 return super.checkSolvable(key)
75
76
77func evaluateSolvability():
78 checkSolvable("")
79
80
81func passedInput(key, skip_focus_check = false):
82 if not symbol_solvable:
83 return
84
85 super.passedInput(key, skip_focus_check)
86
87
88func focus():
89 if not symbol_solvable:
90 has_focus = false
91 return
92
93 super.focus()
94
95
96func unfocus():
97 if not symbol_solvable:
98 has_focus = false
99 return
100
101 super.unfocus()
diff --git a/apworld/client/pauseMenu.gd b/apworld/client/pauseMenu.gd new file mode 100644 index 0000000..50a1e99 --- /dev/null +++ b/apworld/client/pauseMenu.gd
@@ -0,0 +1,110 @@
1extends "res://scripts/ui/pauseMenu.gd"
2
3var compass_button
4var locations_button
5var minimap_button
6var prioritize_current_button
7
8
9func _ready():
10 var ap_panel = Panel.new()
11 ap_panel.name = "Archipelago"
12 get_node("menu/settings/settingsInner/TabContainer").add_child(ap_panel)
13
14 var ap = global.get_node("Archipelago")
15
16 compass_button = CheckBox.new()
17 compass_button.text = "show compass"
18 compass_button.button_pressed = ap.show_compass
19 compass_button.position = Vector2(65, 100)
20 compass_button.theme = preload("res://assets/themes/baseUI.tres")
21 compass_button.add_theme_font_size_override("font_size", 60)
22 compass_button.pressed.connect(_toggle_compass)
23 ap_panel.add_child(compass_button)
24
25 locations_button = CheckBox.new()
26 locations_button.text = "show locations overlay"
27 locations_button.button_pressed = ap.show_locations
28 locations_button.position = Vector2(65, 200)
29 locations_button.theme = preload("res://assets/themes/baseUI.tres")
30 locations_button.add_theme_font_size_override("font_size", 60)
31 locations_button.pressed.connect(_toggle_locations)
32 ap_panel.add_child(locations_button)
33
34 minimap_button = CheckBox.new()
35 minimap_button.text = "show minimap"
36 minimap_button.button_pressed = ap.show_minimap
37 minimap_button.position = Vector2(65, 300)
38 minimap_button.theme = preload("res://assets/themes/baseUI.tres")
39 minimap_button.add_theme_font_size_override("font_size", 60)
40 minimap_button.pressed.connect(_toggle_minimap)
41 ap_panel.add_child(minimap_button)
42
43 prioritize_current_button = CheckBox.new()
44 prioritize_current_button.text = "prioritize locations on current map"
45 prioritize_current_button.button_pressed = ap.prioritize_current_map
46 prioritize_current_button.position = Vector2(65, 400)
47 prioritize_current_button.theme = preload("res://assets/themes/baseUI.tres")
48 prioritize_current_button.add_theme_font_size_override("font_size", 60)
49 prioritize_current_button.pressed.connect(_toggle_prioritize_current)
50 ap_panel.add_child(prioritize_current_button)
51
52 super._ready()
53
54
55func _pause_game():
56 global.get_node("Textclient").dismiss()
57 super._pause_game()
58
59
60func _main_menu():
61 global.loaded = false
62 global.get_node("Archipelago").disconnect_from_ap()
63 global.get_node("Messages").clear()
64 global.get_node("Compass").visible = false
65 global.get_node("Textclient").reset()
66
67 autosplitter.reset()
68 _unpause_game()
69 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
70 musicPlayer.stop()
71
72 var runtime = global.get_node("Runtime")
73 runtime.load_script_as_scene.call_deferred("settings_screen.gd", "settings_screen")
74
75
76func _toggle_compass():
77 var ap = global.get_node("Archipelago")
78 ap.show_compass = compass_button.button_pressed
79 ap.saveSettings()
80
81 var compass = global.get_node("Compass")
82 compass.visible = compass_button.button_pressed
83
84
85func _toggle_locations():
86 var ap = global.get_node("Archipelago")
87 ap.show_locations = locations_button.button_pressed
88 ap.saveSettings()
89
90 var textclient = global.get_node("Textclient")
91 textclient.update_locations_visibility()
92
93
94func _toggle_minimap():
95 var ap = global.get_node("Archipelago")
96 ap.show_minimap = minimap_button.button_pressed
97 ap.saveSettings()
98
99 var minimap = get_tree().get_root().get_node("scene/Minimap")
100 if minimap != null:
101 minimap.visible = ap.show_minimap
102
103
104func _toggle_prioritize_current():
105 var ap = global.get_node("Archipelago")
106 ap.prioritize_current_map = prioritize_current_button.button_pressed
107 ap.saveSettings()
108
109 var textclient = global.get_node("Textclient")
110 textclient.update_locations()
diff --git a/apworld/client/player.gd b/apworld/client/player.gd new file mode 100644 index 0000000..dc05fc9 --- /dev/null +++ b/apworld/client/player.gd
@@ -0,0 +1,222 @@
1extends "res://scripts/nodes/player.gd"
2
3signal evaluate_solvability
4
5var compass
6
7
8func _ready():
9 var khl_script = load("res://scripts/nodes/keyHolderListener.gd")
10
11 var pause_menu = get_node("pause_menu")
12 pause_menu.layer = 3
13
14 var ap = global.get_node("Archipelago")
15 var gamedata = global.get_node("Gamedata")
16 var map_id = gamedata.map_id_by_name.get(global.map)
17 var map_data = gamedata.objects.get_maps()[map_id]
18
19 compass = global.get_node("Compass")
20 compass.visible = ap.show_compass
21
22 ap.start_batching_locations()
23
24 # Run map-specific initialization.
25 var map_script = ap.get_map_script(global.map)
26 if map_script != null:
27 map_script.on_map_load(get_tree().get_root())
28
29 ap.update_job_well_done_sign()
30
31 # Set up the RTE trigger, if there is one.
32 if map_data.has_rte_trigger_pos():
33 var oneShotListener_prefab = preload("res://objects/nodes/listeners/oneShotListener.tscn")
34 var triggerArea_prefab = preload("res://objects/nodes/triggerArea.tscn")
35 var unlockSetterListener_prefab = preload(
36 "res://objects/nodes/listeners/unlockSetterListener.tscn"
37 )
38
39 var triggerArea = triggerArea_prefab.instantiate()
40 triggerArea.name = "rte_triggerArea"
41 triggerArea.position = gamedata.vec3d_to_vector3(map_data.get_rte_trigger_pos())
42 triggerArea.scale = gamedata.vec3d_to_vector3(map_data.get_rte_trigger_scale())
43 get_parent().add_child.call_deferred(triggerArea)
44
45 var osl = oneShotListener_prefab.instantiate()
46 osl.name = "rte_osl"
47 osl.senders.append(NodePath("/root/scene/rte_triggerArea"))
48 get_parent().add_child.call_deferred(osl)
49
50 var usl = unlockSetterListener_prefab.instantiate()
51 usl.name = "rte_usl"
52 usl.key = "rte_%s" % global.map
53 usl.value = "unlocked"
54 usl.senders.append(NodePath("/root/scene/rte_osl"))
55 get_parent().add_child.call_deferred(usl)
56
57 # Set up door locations.
58 for door in gamedata.objects.get_doors():
59 if door.get_map_id() != map_id:
60 continue
61
62 if not door.has_ap_id():
63 continue
64
65 if (
66 not (door.has_legacy_location() and door.get_legacy_location())
67 and (
68 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
69 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
70 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
71 )
72 ):
73 continue
74
75 var locationListener = ap.SCRIPT_locationListener.new()
76 locationListener.location_id = door.get_ap_id()
77 locationListener.name = "locationListener_%d" % door.get_ap_id()
78
79 for panel_ref in door.get_panels():
80 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
81 var panel_path = panel_data.get_path()
82
83 if panel_ref.has_answer():
84 for proxy in panel_data.get_proxies():
85 if proxy.get_answer() == panel_ref.get_answer():
86 panel_path = proxy.get_path()
87 break
88
89 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
90
91 for keyholder_ref in door.get_keyholders():
92 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
93
94 var khl = khl_script.new()
95 khl.name = (
96 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
97 )
98 khl.answer = keyholder_ref.get_key()
99 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
100 get_parent().add_child.call_deferred(khl)
101
102 locationListener.senders.append(NodePath("../" + khl.name))
103
104 for sender in door.get_senders():
105 locationListener.senders.append(NodePath("/root/scene/" + sender))
106
107 if door.has_complete_at():
108 locationListener.complete_at = door.get_complete_at()
109
110 get_parent().add_child.call_deferred(locationListener)
111
112 # Set up letter locations.
113 for letter in gamedata.objects.get_letters():
114 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
115 if room.get_map_id() != map_id:
116 continue
117
118 var locationListener = ap.SCRIPT_locationListener.new()
119 locationListener.location_id = letter.get_ap_id()
120 locationListener.name = "locationListener_%d" % letter.get_ap_id()
121 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
122
123 get_parent().add_child.call_deferred(locationListener)
124
125 if (
126 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
127 != ap.kLETTER_BEHAVIOR_VANILLA
128 ):
129 var scout = ap.scout_location(letter.get_ap_id())
130 if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0):
131 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
132 letter.get_path()
133 )
134 if collectable != null:
135 collectable.setScoutedText.call_deferred(scout["item"])
136
137 # Set up mastery locations.
138 for mastery in gamedata.objects.get_masteries():
139 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
140 if room.get_map_id() != map_id:
141 continue
142
143 var locationListener = ap.SCRIPT_locationListener.new()
144 locationListener.location_id = mastery.get_ap_id()
145 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
146 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
147
148 get_parent().add_child.call_deferred(locationListener)
149
150 # Set up ending locations.
151 for ending in gamedata.objects.get_endings():
152 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
153 if room.get_map_id() != map_id:
154 continue
155
156 var locationListener = ap.SCRIPT_locationListener.new()
157 locationListener.location_id = ending.get_ap_id()
158 locationListener.name = "locationListener_%d" % ending.get_ap_id()
159 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
160
161 get_parent().add_child.call_deferred(locationListener)
162
163 if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
164 var victoryListener = ap.SCRIPT_victoryListener.new()
165 victoryListener.name = "victoryListener"
166 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
167
168 get_parent().add_child.call_deferred(victoryListener)
169
170 # Set up keyholder locations, in keyholder sanity.
171 if ap.keyholder_sanity:
172 for keyholder in gamedata.objects.get_keyholders():
173 if not keyholder.has_key():
174 continue
175
176 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
177 if room.get_map_id() != map_id:
178 continue
179
180 var locationListener = ap.SCRIPT_locationListener.new()
181 locationListener.location_id = keyholder.get_ap_id()
182 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
183
184 var khl = khl_script.new()
185 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
186 khl.answer = keyholder.get_key()
187 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
188 get_parent().add_child.call_deferred(khl)
189
190 locationListener.senders.append(NodePath("../" + khl.name))
191
192 get_parent().add_child.call_deferred(locationListener)
193
194 var minimap = ap.SCRIPT_minimap.new()
195 minimap.name = "Minimap"
196 minimap.visible = ap.show_minimap
197 get_parent().add_child.call_deferred(minimap)
198
199 var textclient = global.get_node("Textclient")
200 textclient.update_locations()
201
202 if ap.music_mapping.has(global.map):
203 var song_setter = get_node_or_null("/root/scene/songSetter")
204 if song_setter:
205 song_setter.song_name = ap.music_mapping[global.map]
206 else:
207 var song_setter_prefab = preload("res://objects/nodes/songSetter.tscn")
208 song_setter = song_setter_prefab.instantiate()
209 song_setter.name = "songSetter"
210 song_setter.song_name = ap.music_mapping[global.map]
211 get_parent().add_child.call_deferred(song_setter)
212
213 super._ready()
214
215 await get_tree().process_frame
216 await get_tree().process_frame
217
218 ap.stop_batching_locations()
219
220
221func _process(_dt):
222 compass.update_rotation(global_rotation.y)
diff --git a/apworld/client/rainbowText.gd b/apworld/client/rainbowText.gd new file mode 100644 index 0000000..9a4c1d0 --- /dev/null +++ b/apworld/client/rainbowText.gd
@@ -0,0 +1,10 @@
1extends RichTextEffect
2
3var bbcode = "rainbow"
4
5
6func _process_custom_fx(char_fx: CharFXTransform):
7 char_fx.color = Color.from_hsv(
8 char_fx.elapsed_time - floor(char_fx.elapsed_time), 1.0, 1.0, 1.0
9 )
10 return true
diff --git a/apworld/client/rteMenu.gd b/apworld/client/rteMenu.gd new file mode 100644 index 0000000..519f09f --- /dev/null +++ b/apworld/client/rteMenu.gd
@@ -0,0 +1,67 @@
1extends "res://scripts/ui/rteMenu.gd"
2
3var buttons = []
4
5
6func _readier():
7 var ap = global.get_node("Archipelago")
8 if ap.daedalus_only:
9 get_node("rte_the_entry").hide()
10 get_node("rte_daedalus").show()
11
12 switcher.preload_map("res://objects/scenes/daedalus.tscn")
13 elif !ap.rte_mapping.is_empty():
14 buttons = [$rte_the_plaza, $rte_the_gallery, $rte_daedalus, $rte_control_center]
15 for i in range(4):
16 buttons[i].name = "button_%d" % i
17 for i in range(4):
18 _setupButton(buttons[i], ap.rte_mapping[i])
19
20 refreshButtons()
21 else:
22 super()._readier()
23
24
25func _setupButton(button, map_name):
26 switcher.preload_map("res://objects/scenes/%s.tscn" % map_name)
27
28 button.hide()
29 button.text = map_name.replace("_", " ")
30 button.name = "rte_%s" % map_name
31 button.autowrap_mode = TextServer.AUTOWRAP_WORD
32
33 var ap = global.get_node("Archipelago")
34 if (
35 ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_VANILLA
36 and !unlocks.data.has("rte_%s" % map_name)
37 ):
38 unlocks.data["rte_%s" % map_name] = ""
39
40
41func refreshButtons():
42 var ap = global.get_node("Archipelago")
43 if ap.rte_mapping.is_empty():
44 return
45
46 for i in range(4):
47 if _shouldShowButton(ap.rte_mapping[i]):
48 buttons[i].show()
49 else:
50 buttons[i].hide()
51
52
53func _shouldShowButton(map_name):
54 var ap = global.get_node("Archipelago")
55
56 if ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_VANILLA:
57 return unlocks.data["rte_%s" % map_name] == "unlocked"
58 elif ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_UNLOCKED:
59 return true
60 elif ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_ITEMS:
61 var gamedata = global.get_node("Gamedata")
62 var map_id = gamedata.map_id_by_name[map_name]
63 var rte_ap_id = gamedata.objects.get_maps()[map_id].get_rte_ap_id()
64
65 return ap.client.hasItem(rte_ap_id)
66
67 return false
diff --git a/apworld/client/run_from_apworld.tscn b/apworld/client/run_from_apworld.tscn new file mode 100644 index 0000000..11373e0 --- /dev/null +++ b/apworld/client/run_from_apworld.tscn
@@ -0,0 +1,30 @@
1[gd_scene load_steps=11 format=2]
2
3[sub_resource id=2 type="GDScript"]
4script/source = "extends Node2D
5
6
7func _ready():
8 var args = OS.get_cmdline_user_args()
9 var apworld_path = args[0]
10
11 var zip_reader = ZIPReader.new()
12 zip_reader.open(apworld_path)
13
14 var runtime_script = GDScript.new()
15 runtime_script.source_code = zip_reader.read_file(\"lingo2/client/apworld_runtime.gd\").get_string_from_utf8()
16 runtime_script.reload()
17
18 zip_reader.close()
19
20 var runtime = runtime_script.new(apworld_path)
21 runtime.name = \"Runtime\"
22
23 global.add_child(runtime)
24
25 runtime.load_script_as_scene.call_deferred(\"settings_screen.gd\", \"settings_screen\")
26
27"
28
29[node name="loader" type="Node2D"]
30script = SubResource( 2 )
diff --git a/apworld/client/run_from_source.tscn b/apworld/client/run_from_source.tscn new file mode 100644 index 0000000..59a914d --- /dev/null +++ b/apworld/client/run_from_source.tscn
@@ -0,0 +1,22 @@
1[gd_scene load_steps=11 format=2]
2
3[sub_resource id=2 type="GDScript"]
4script/source = "extends Node2D
5
6
7func _ready():
8 var args = OS.get_cmdline_user_args()
9 var source_path = args[0]
10
11 var runtime_script = ResourceLoader.load(\"%s/source_runtime.gd\" % source_path)
12 var runtime = runtime_script.new(source_path)
13 runtime.name = \"Runtime\"
14
15 global.add_child(runtime)
16
17 runtime.load_script_as_scene.call_deferred(\"settings_screen.gd\", \"settings_screen\")
18
19"
20
21[node name="loader" type="Node2D"]
22script = SubResource( 2 )
diff --git a/apworld/client/saver.gd b/apworld/client/saver.gd new file mode 100644 index 0000000..44bc179 --- /dev/null +++ b/apworld/client/saver.gd
@@ -0,0 +1,23 @@
1extends "res://scripts/nodes/saver.gd"
2
3
4func levelLoaded():
5 if type == "keyholders":
6 var ap = global.get_node("Archipelago")
7 ap.keyboard.load_keyholders.call_deferred(global.map)
8 else:
9 reload.call_deferred()
10
11
12func reload():
13 # Just rewriting this whole thing so I can remove Chris's safeguard.
14 var file = FileAccess.open(path + type + ".save", FileAccess.READ)
15 if file:
16 var data = file.get_var(true)
17 file.close()
18 for datum in data:
19 var saveable = get_node_or_null(datum[0])
20 if saveable != null:
21 saveable.is_complete = datum[1]
22 if saveable.is_complete:
23 saveable.loadData(saveable.is_complete)
diff --git a/apworld/client/settings_screen.gd b/apworld/client/settings_screen.gd new file mode 100644 index 0000000..89e8b68 --- /dev/null +++ b/apworld/client/settings_screen.gd
@@ -0,0 +1,149 @@
1extends Node
2
3
4func _ready():
5 var theme = preload("res://assets/themes/baseUI.tres")
6
7 var simple_style_box = StyleBoxFlat.new()
8 simple_style_box.bg_color = Color(0, 0, 0, 0)
9
10 var panel = Panel.new()
11 panel.name = "Panel"
12 panel.offset_right = 1920.0
13 panel.offset_bottom = 1080.0
14 add_child(panel)
15
16 var title = Label.new()
17 title.name = "title"
18 title.offset_left = 0.0
19 title.offset_top = 75.0
20 title.offset_right = 1920.0
21 title.offset_bottom = 225.0
22 title.text = "ARCHIPELAGO"
23 title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
24 title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
25 title.theme = theme
26 panel.add_child(title)
27
28 var connect_button = Button.new()
29 connect_button.name = "connect_button"
30 connect_button.offset_left = 255.0
31 connect_button.offset_top = 875.0
32 connect_button.offset_right = 891.0
33 connect_button.offset_bottom = 1025.0
34 connect_button.add_theme_color_override("font_color_hover", Color(1, 0.501961, 0, 1))
35 connect_button.text = "CONNECT"
36 connect_button.theme = theme
37 panel.add_child(connect_button)
38
39 var quit_button = Button.new()
40 quit_button.name = "quit_button"
41 quit_button.offset_left = 1102.0
42 quit_button.offset_top = 875.0
43 quit_button.offset_right = 1738.0
44 quit_button.offset_bottom = 1025.0
45 quit_button.add_theme_color_override("font_color_hover", Color(1, 0, 0, 1))
46 quit_button.text = "QUIT"
47 quit_button.theme = theme
48 panel.add_child(quit_button)
49
50 var credit2 = Label.new()
51 credit2.name = "credit2"
52 credit2.offset_left = -105.0
53 credit2.offset_top = 346.0
54 credit2.offset_right = 485.0
55 credit2.offset_bottom = 410.0
56 credit2.add_theme_stylebox_override("normal", simple_style_box)
57 credit2.text = "SERVER"
58 credit2.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
59 credit2.theme = theme
60 panel.add_child(credit2)
61
62 var credit3 = Label.new()
63 credit3.name = "credit3"
64 credit3.offset_left = -105.0
65 credit3.offset_top = 519.0
66 credit3.offset_right = 485.0
67 credit3.offset_bottom = 583.0
68 credit3.add_theme_stylebox_override("normal", simple_style_box)
69 credit3.text = "PLAYER"
70 credit3.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
71 credit3.theme = theme
72 panel.add_child(credit3)
73
74 var credit4 = Label.new()
75 credit4.name = "credit4"
76 credit4.offset_left = -105.0
77 credit4.offset_top = 704.0
78 credit4.offset_right = 485.0
79 credit4.offset_bottom = 768.0
80 credit4.add_theme_stylebox_override("normal", simple_style_box)
81 credit4.text = "PASSWORD"
82 credit4.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
83 credit4.theme = theme
84 panel.add_child(credit4)
85
86 var credit5 = Label.new()
87 credit5.name = "credit5"
88 credit5.offset_left = 1239.0
89 credit5.offset_top = 422.0
90 credit5.offset_right = 1829.0
91 credit5.offset_bottom = 486.0
92 credit5.add_theme_stylebox_override("normal", simple_style_box)
93 credit5.text = "OPTIONS"
94 credit5.theme = theme
95 panel.add_child(credit5)
96
97 var server_box = LineEdit.new()
98 server_box.name = "server_box"
99 server_box.offset_left = 502.0
100 server_box.offset_top = 295.0
101 server_box.offset_right = 1144.0
102 server_box.offset_bottom = 445.0
103 server_box.alignment = HORIZONTAL_ALIGNMENT_CENTER
104 server_box.caret_blink = true
105 panel.add_child(server_box)
106
107 var player_box = LineEdit.new()
108 player_box.name = "player_box"
109 player_box.offset_left = 502.0
110 player_box.offset_top = 477.0
111 player_box.offset_right = 1144.0
112 player_box.offset_bottom = 627.0
113 player_box.alignment = HORIZONTAL_ALIGNMENT_CENTER
114 player_box.caret_blink = true
115 panel.add_child(player_box)
116
117 var password_box = LineEdit.new()
118 password_box.name = "password_box"
119 password_box.offset_left = 502.0
120 password_box.offset_top = 659.0
121 password_box.offset_right = 1144.0
122 password_box.offset_bottom = 809.0
123 password_box.alignment = HORIZONTAL_ALIGNMENT_CENTER
124 password_box.caret_blink = true
125 panel.add_child(password_box)
126
127 var accept_dialog = AcceptDialog.new()
128 accept_dialog.name = "AcceptDialog"
129 panel.add_child(accept_dialog)
130
131 var version_mismatch = ConfirmationDialog.new()
132 version_mismatch.name = "VersionMismatch"
133 panel.add_child(version_mismatch)
134
135 var connection_history = MenuButton.new()
136 connection_history.name = "connection_history"
137 connection_history.offset_left = 1239.0
138 connection_history.offset_top = 276.0
139 connection_history.offset_right = 1829.0
140 connection_history.offset_bottom = 372.0
141 connection_history.text = "connection history"
142 connection_history.flat = false
143 panel.add_child(connection_history)
144
145 var runtime = global.get_node("Runtime")
146 var main_script = runtime.load_script("main.gd")
147 var main_node = main_script.new()
148 main_node.name = "Main"
149 add_child(main_node)
diff --git a/apworld/client/source_runtime.gd b/apworld/client/source_runtime.gd new file mode 100644 index 0000000..146587a --- /dev/null +++ b/apworld/client/source_runtime.gd
@@ -0,0 +1,33 @@
1extends Node
2
3var source_path
4
5
6func _init(path):
7 source_path = path
8
9
10func path_exists(path):
11 return FileAccess.file_exists("%s/%s" % [source_path, path])
12
13
14func load_script(path):
15 return ResourceLoader.load("%s/%s" % [source_path, path])
16
17
18func read_path(path):
19 return FileAccess.get_file_as_bytes("%s/%s" % [source_path, path])
20
21
22func load_script_as_scene(path, scene_name):
23 var script = load_script(path)
24 var instance = script.new()
25 instance.name = scene_name
26
27 get_tree().unload_current_scene()
28 _load_scene.call_deferred(instance)
29
30
31func _load_scene(instance):
32 get_tree().get_root().add_child(instance)
33 get_tree().current_scene = instance
diff --git a/apworld/client/teleport.gd b/apworld/client/teleport.gd new file mode 100644 index 0000000..428d50b --- /dev/null +++ b/apworld/client/teleport.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/teleport.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
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/apworld/client/teleportListener.gd b/apworld/client/teleportListener.gd new file mode 100644 index 0000000..6f363af --- /dev/null +++ b/apworld/client/teleportListener.gd
@@ -0,0 +1,49 @@
1extends "res://scripts/nodes/listeners/teleportListener.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 if (
13 global.map == "daedalus"
14 and (
15 node_path == "Components/Triggers/teleportListenerConnections"
16 or node_path == "Components/Triggers/teleportListenerConnections2"
17 )
18 ):
19 # Effectively disable these.
20 teleport_point = target_path.position
21 return
22
23 var gamedata = global.get_node("Gamedata")
24 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
25 if door_id != null:
26 var ap = global.get_node("Archipelago")
27 var item_lock = ap.get_item_id_for_door(door_id)
28
29 if item_lock != null:
30 item_id = item_lock[0]
31 item_amount = item_lock[1]
32
33 self.senders = []
34 self.senderGroup = []
35 self.nested = false
36 self.complete_at = 0
37 self.max_length = 0
38 self.excludeSenders = []
39
40 call_deferred("_readier")
41
42 super._ready()
43
44
45func _readier():
46 var ap = global.get_node("Archipelago")
47
48 if ap.client.getItemAmount(item_id) >= item_amount:
49 handleTriggered()
diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd new file mode 100644 index 0000000..571f3d6 --- /dev/null +++ b/apworld/client/textclient.gd
@@ -0,0 +1,514 @@
1extends CanvasLayer
2
3var tabs
4var panel
5var label
6var entry
7var is_open = false
8
9var locations_overlay
10var location_texture
11var worldport_texture
12var goal_texture
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
22var worldports_tab
23var worldports_tree
24var port_tree_item_by_map = {}
25var port_tree_item_by_map_port = {}
26
27const kLocation = 0
28const kWorldport = 1
29const kGoal = 2
30
31
32func _ready():
33 process_mode = ProcessMode.PROCESS_MODE_ALWAYS
34 layer = 2
35
36 locations_overlay = RichTextLabel.new()
37 locations_overlay.name = "LocationsOverlay"
38 locations_overlay.offset_top = 220
39 locations_overlay.offset_bottom = 720
40 locations_overlay.offset_left = 20
41 locations_overlay.anchor_right = 1.0
42 locations_overlay.offset_right = -10
43 locations_overlay.scroll_active = false
44 locations_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
45 locations_overlay.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
46 add_child(locations_overlay)
47 update_locations_visibility()
48
49 tabs = TabContainer.new()
50 tabs.name = "Tabs"
51 tabs.offset_left = 100
52 tabs.offset_right = 1820
53 tabs.offset_top = 100
54 tabs.offset_bottom = 980
55 tabs.visible = false
56 tabs.theme = preload("res://assets/themes/baseUI.tres")
57 tabs.add_theme_font_size_override("font_size", 36)
58 add_child(tabs)
59
60 panel = MarginContainer.new()
61 panel.name = "Text Client"
62 panel.add_theme_constant_override("margin_top", 60)
63 panel.add_theme_constant_override("margin_left", 60)
64 panel.add_theme_constant_override("margin_right", 60)
65 panel.add_theme_constant_override("margin_bottom", 60)
66 tabs.add_child(panel)
67
68 label = RichTextLabel.new()
69 label.set_name("Label")
70 label.scroll_following = true
71 label.selection_enabled = true
72 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
73 label.size_flags_vertical = Control.SIZE_EXPAND_FILL
74 label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
75 label.push_font_size(30)
76
77 var entry_style = StyleBoxFlat.new()
78 entry_style.bg_color = Color(0.9, 0.9, 0.9, 1)
79
80 entry = LineEdit.new()
81 entry.set_name("Entry")
82 entry.add_theme_font_override("font", preload("res://assets/fonts/Lingo2.ttf"))
83 entry.add_theme_font_size_override("font_size", 36)
84 entry.add_theme_color_override("font_color", Color(0, 0, 0, 1))
85 entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1))
86 entry.add_theme_stylebox_override("focus", entry_style)
87 entry.text_submitted.connect(text_entered)
88
89 var tc_arranger = VBoxContainer.new()
90 tc_arranger.add_child(label)
91 tc_arranger.add_child(entry)
92 tc_arranger.add_theme_constant_override("separation", 40)
93 panel.add_child(tc_arranger)
94
95 var tracker_margins = MarginContainer.new()
96 tracker_margins.name = "Locations"
97 tracker_margins.add_theme_constant_override("margin_top", 60)
98 tracker_margins.add_theme_constant_override("margin_left", 60)
99 tracker_margins.add_theme_constant_override("margin_right", 60)
100 tracker_margins.add_theme_constant_override("margin_bottom", 60)
101 tabs.add_child(tracker_margins)
102
103 tracker_tree = Tree.new()
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)
117
118 worldports_tab = MarginContainer.new()
119 worldports_tab.name = "Worldports"
120 worldports_tab.add_theme_constant_override("margin_top", 60)
121 worldports_tab.add_theme_constant_override("margin_left", 60)
122 worldports_tab.add_theme_constant_override("margin_right", 60)
123 worldports_tab.add_theme_constant_override("margin_bottom", 60)
124 tabs.add_child(worldports_tab)
125 tabs.set_tab_hidden(2, true)
126
127 worldports_tree = Tree.new()
128 worldports_tree.columns = 2
129 worldports_tree.hide_root = true
130 worldports_tree.theme = preload("res://assets/themes/baseUI.tres")
131 worldports_tree.add_theme_font_size_override("font_size", 24)
132 worldports_tab.add_child(worldports_tree)
133
134 var runtime = global.get_node("Runtime")
135 var location_image = Image.new()
136 location_image.load_png_from_buffer(runtime.read_path("assets/location.png"))
137 location_texture = ImageTexture.create_from_image(location_image)
138
139 var worldport_image = Image.new()
140 worldport_image.load_png_from_buffer(runtime.read_path("assets/worldport.png"))
141 worldport_texture = ImageTexture.create_from_image(worldport_image)
142
143 var goal_image = Image.new()
144 goal_image.load_png_from_buffer(runtime.read_path("assets/goal.png"))
145 goal_texture = ImageTexture.create_from_image(goal_image)
146
147
148func _input(event):
149 if global.loaded and event is InputEventKey and event.pressed:
150 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
151 if !get_tree().paused:
152 is_open = true
153 get_tree().paused = true
154 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
155 tabs.visible = true
156 entry.grab_focus()
157 get_viewport().set_input_as_handled()
158 else:
159 dismiss()
160 elif event.keycode == KEY_ESCAPE:
161 if is_open:
162 dismiss()
163 get_viewport().set_input_as_handled()
164
165
166func dismiss():
167 if is_open:
168 get_tree().paused = false
169 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
170 tabs.visible = false
171 is_open = false
172
173
174func parse_printjson(text):
175 label.append_text("[p]" + text + "[/p]")
176
177
178func text_entered(text):
179 var ap = global.get_node("Archipelago")
180 var cmd = text.trim_suffix("\n")
181 entry.text = ""
182 if OS.is_debug_build():
183 if cmd.begins_with("/tp_map "):
184 var new_map = cmd.substr(8)
185 global.map = new_map
186 global.sets_entry_point = false
187 switcher.switch_map("res://objects/scenes/%s.tscn" % new_map)
188 return
189
190 ap.client.say(cmd)
191
192
193func update_locations(reset_locations = true):
194 var ap = global.get_node("Archipelago")
195 var gamedata = global.get_node("Gamedata")
196
197 locations_overlay.clear()
198 locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf"))
199 locations_overlay.push_font_size(24)
200 locations_overlay.push_color(Color(0.9, 0.9, 0.9, 1))
201 locations_overlay.push_outline_color(Color(0, 0, 0, 1))
202 locations_overlay.push_outline_size(2)
203
204 var locations = []
205 for location_id in ap.client._accessible_locations:
206 if not ap.client._checked_locations.has(location_id):
207 var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)")
208 var map_name = gamedata.get_map_name_for_location(location_id)
209 (
210 locations
211 . append(
212 {
213 "name": location_name,
214 "type": kLocation,
215 "id": location_id,
216 "ignored": ap._ignored_locations.has(location_id),
217 "hint": ap.client._hinted_locations.has(location_id),
218 "current": ap.prioritize_current_map and map_name == global.map,
219 }
220 )
221 )
222
223 for port_id in ap.client._accessible_worldports:
224 if not ap.client._checked_worldports.has(port_id):
225 var port_name = gamedata.get_worldport_display_name(port_id)
226 var map_name = gamedata.get_map_name_for_port(port_id)
227 (
228 locations
229 . append(
230 {
231 "name": port_name,
232 "type": kWorldport,
233 "id": port_id,
234 "ignored": false,
235 "hint": false,
236 "current": ap.prioritize_current_map and map_name == global.map,
237 }
238 )
239 )
240
241 locations.sort_custom(_cmp_tracker_objects)
242
243 if ap.client._goal_accessible:
244 var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
245 ap.victory_condition
246 ]]
247 (
248 locations
249 . push_front(
250 {
251 "name": location_name,
252 "type": kGoal,
253 "ignored": false,
254 "hint": false,
255 }
256 )
257 )
258
259 var count = 0
260 for location in locations:
261 if count < 18 and not location["ignored"]:
262 locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT)
263 if location["hint"]:
264 locations_overlay.push_color(Color("#fafad2"))
265 locations_overlay.append_text(location["name"])
266 locations_overlay.append_text(" ")
267 if location["type"] == kLocation:
268 locations_overlay.add_image(location_texture)
269 elif location["type"] == kWorldport:
270 locations_overlay.add_image(worldport_texture)
271 elif location["type"] == kGoal:
272 locations_overlay.add_image(goal_texture)
273 if location["hint"]:
274 locations_overlay.pop()
275 locations_overlay.pop()
276 count += 1
277
278 if count > 18:
279 locations_overlay.append_text("[p align=right][lb]...[rb][/p]")
280
281 if reset_locations:
282 reset_tracker_tab()
283
284 var root_ti = tracker_tree.create_item(null)
285
286 for location in locations:
287 var loc_row
288
289 if location["ignored"]:
290 if tracker_ignored_group == null:
291 tracker_ignored_group = root_ti.create_child()
292 tracker_ignored_group.set_text(1, "Ignored Locations")
293 tracker_ignored_group.set_selectable(0, false)
294 tracker_ignored_group.set_selectable(1, false)
295 tracker_ignored_group.set_selectable(2, false)
296 tracker_ignored_group.set_selectable(3, false)
297
298 loc_row = tracker_ignored_group.create_child()
299 else:
300 loc_row = root_ti.create_child()
301
302 loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON)
303 loc_row.set_selectable(0, false)
304 loc_row.set_text(1, location["name"])
305 loc_row.set_selectable(1, false)
306 if location["hint"]:
307 loc_row.set_custom_color(1, Color("#fafad2"))
308 loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM)
309 loc_row.set_text(2, "Show Path")
310 loc_row.set_custom_as_button(2, true)
311 loc_row.set_editable(2, true)
312 loc_row.set_selectable(2, false)
313 loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER)
314 loc_row.set_selectable(3, false)
315 if location["type"] == kLocation:
316 loc_row.set_cell_mode(3, TreeItem.CELL_MODE_CUSTOM)
317 if location["ignored"]:
318 loc_row.set_text(3, "Unignore")
319 else:
320 loc_row.set_text(3, "Ignore")
321 loc_row.set_custom_as_button(3, true)
322 loc_row.set_editable(3, true)
323 loc_row.set_text_alignment(3, HORIZONTAL_ALIGNMENT_CENTER)
324
325 if location["type"] == kLocation:
326 loc_row.set_icon(0, location_texture)
327 tracker_loc_tree_item_by_id[location["id"]] = loc_row
328 elif location["type"] == kWorldport:
329 loc_row.set_icon(0, worldport_texture)
330 tracker_port_tree_item_by_id[location["id"]] = loc_row
331 elif location["type"] == kGoal:
332 loc_row.set_icon(0, goal_texture)
333 tracker_goal_tree_item = loc_row
334
335 if location["ignored"]:
336 tracker_object_by_ignored_index[loc_row.get_index()] = location
337 else:
338 tracker_object_by_index[loc_row.get_index()] = location
339 else:
340 for loc_row in tracker_tree.get_root().get_children():
341 loc_row.visible = false
342
343 for location_id in tracker_loc_tree_item_by_id.keys():
344 if (
345 ap.client._accessible_locations.has(location_id)
346 and not ap.client._checked_locations.has(location_id)
347 ):
348 tracker_loc_tree_item_by_id[location_id].visible = true
349
350 for port_id in tracker_port_tree_item_by_id.keys():
351 if (
352 ap.client._accessible_worldports.has(port_id)
353 and not ap.client._checked_worldports.has(port_id)
354 ):
355 tracker_port_tree_item_by_id[port_id].visible = true
356
357 if tracker_goal_tree_item != null and ap.client._goal_accessible:
358 tracker_goal_tree_item.visible = true
359
360 if tracker_ignored_group != null:
361 tracker_ignored_group.visible = true
362
363
364func _cmp_tracker_objects(a, b) -> bool:
365 if a["ignored"] != b["ignored"]:
366 return !a["ignored"]
367 elif a["hint"] != b["hint"]:
368 return a["hint"]
369 elif a["current"] != b["current"]:
370 return a["current"]
371 else:
372 return a["name"] < b["name"]
373
374
375func update_locations_visibility():
376 var ap = global.get_node("Archipelago")
377 locations_overlay.visible = ap.show_locations
378
379
380func _on_tracker_button_clicked():
381 var ap = global.get_node("Archipelago")
382
383 var edited_item = tracker_tree.get_edited()
384 var edited_index = edited_item.get_index()
385
386 if edited_item.get_parent() == tracker_tree.get_root():
387 if tracker_object_by_index.has(edited_index):
388 var tracker_object = tracker_object_by_index[edited_index]
389 if tracker_tree.get_edited_column() == 2:
390 var type_str = ""
391 if tracker_object["type"] == kLocation:
392 type_str = "location"
393 elif tracker_object["type"] == kWorldport:
394 type_str = "worldport"
395 elif tracker_object["type"] == kGoal:
396 type_str = "goal"
397 ap.client.getLogicalPath(type_str, tracker_object.get("id", null))
398 elif tracker_tree.get_edited_column() == 3:
399 ap.toggle_ignored_location(tracker_object["id"])
400 elif edited_item.get_parent() == tracker_ignored_group:
401 # This is the ignored locations group.
402 if (
403 tracker_object_by_ignored_index.has(edited_index)
404 and tracker_tree.get_edited_column() == 3
405 ):
406 var tracker_object = tracker_object_by_ignored_index[edited_index]
407 ap.toggle_ignored_location(tracker_object["id"])
408
409
410func display_logical_path(object_type, object_id, paths):
411 var ap = global.get_node("Archipelago")
412 var gamedata = global.get_node("Gamedata")
413
414 var location_name = "(Unknown)"
415 if object_type == "location" and object_id != null:
416 location_name = gamedata.location_name_by_id.get(object_id, "(Unknown)")
417 elif object_type == "worldport" and object_id != null:
418 location_name = gamedata.get_worldport_display_name(object_id)
419 elif object_type == "goal":
420 location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
421 ap.victory_condition
422 ]]
423
424 label.append_text("[p]Path to %s:[/p]" % location_name)
425 label.append_text("[ol]" + "\n".join(paths) + "[/ol]")
426
427 panel.visible = true
428
429
430func setup_worldports():
431 tabs.set_tab_hidden(2, false)
432
433 var root_ti = worldports_tree.create_item(null)
434
435 var ports_by_map_id = {}
436 var display_names_by_map_id = {}
437 var display_names_by_port_id = {}
438
439 var ap = global.get_node("Archipelago")
440 var gamedata = global.get_node("Gamedata")
441 for fpid in ap.port_pairings:
442 var port = gamedata.objects.get_ports()[fpid]
443 var room = gamedata.objects.get_rooms()[port.get_room_id()]
444
445 if not ports_by_map_id.has(room.get_map_id()):
446 ports_by_map_id[room.get_map_id()] = []
447
448 var map = gamedata.objects.get_maps()[room.get_map_id()]
449 display_names_by_map_id[map.get_id()] = map.get_display_name()
450
451 ports_by_map_id[room.get_map_id()].append(fpid)
452 display_names_by_port_id[fpid] = port.get_display_name()
453
454 var sorted_map_ids = ports_by_map_id.keys().duplicate()
455 sorted_map_ids.sort_custom(
456 func(a, b): return display_names_by_map_id[a] < display_names_by_map_id[b]
457 )
458
459 for map_id in sorted_map_ids:
460 var map_ti = root_ti.create_child()
461 map_ti.set_text(0, display_names_by_map_id[map_id])
462 map_ti.visible = false
463 map_ti.collapsed = true
464 port_tree_item_by_map[map_id] = map_ti
465 port_tree_item_by_map_port[map_id] = {}
466
467 var port_ids = ports_by_map_id[map_id]
468 port_ids.sort_custom(
469 func(a, b): return display_names_by_port_id[a] < display_names_by_port_id[b]
470 )
471
472 for port_id in port_ids:
473 var port_ti = map_ti.create_child()
474 port_ti.set_text(0, display_names_by_port_id[port_id])
475 port_ti.set_text(1, gamedata.get_worldport_display_name(ap.port_pairings[port_id]))
476 port_ti.visible = false
477 port_tree_item_by_map_port[map_id][port_id] = port_ti
478
479 update_worldports()
480
481
482func update_worldports():
483 var ap = global.get_node("Archipelago")
484
485 for map_id in port_tree_item_by_map_port.keys():
486 var map_visible = false
487
488 for port_id in port_tree_item_by_map_port[map_id].keys():
489 var ti = port_tree_item_by_map_port[map_id][port_id]
490 ti.visible = ap.client._checked_worldports.has(port_id)
491
492 if ti.visible:
493 map_visible = true
494
495 port_tree_item_by_map[map_id].visible = map_visible
496
497
498func reset():
499 locations_overlay.clear()
500 tabs.set_tab_hidden(2, true)
501 port_tree_item_by_map.clear()
502 port_tree_item_by_map_port.clear()
503 worldports_tree.clear()
504 reset_tracker_tab()
505
506
507func reset_tracker_tab():
508 tracker_loc_tree_item_by_id.clear()
509 tracker_port_tree_item_by_id.clear()
510 tracker_goal_tree_item = null
511 tracker_object_by_index.clear()
512 tracker_object_by_ignored_index.clear()
513 tracker_ignored_group = null
514 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/vendor/LICENSE b/apworld/client/vendor/LICENSE new file mode 100644 index 0000000..12763b1 --- /dev/null +++ b/apworld/client/vendor/LICENSE
@@ -0,0 +1,21 @@
1WebSocketServer.gd:
2
3Copyright (c) 2014-present Godot Engine contributors. Copyright (c) 2007-2014
4Juan Linietsky, Ariel Manzur.
5
6Permission is hereby granted, free of charge, to any person obtaining a copy of
7this software and associated documentation files (the "Software"), to deal in
8the Software without restriction, including without limitation the rights to
9use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10the Software, and to permit persons to whom the Software is furnished to do so,
11subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/apworld/client/vendor/WebSocketServer.gd b/apworld/client/vendor/WebSocketServer.gd new file mode 100644 index 0000000..2cee494 --- /dev/null +++ b/apworld/client/vendor/WebSocketServer.gd
@@ -0,0 +1,173 @@
1class_name WebSocketServer
2extends Node
3
4signal message_received(peer_id: int, message: String)
5signal client_connected(peer_id: int)
6signal client_disconnected(peer_id: int)
7
8@export var handshake_headers := PackedStringArray()
9@export var supported_protocols := PackedStringArray()
10@export var handshake_timout := 3000
11@export var use_tls := false
12@export var tls_cert: X509Certificate
13@export var tls_key: CryptoKey
14@export var refuse_new_connections := false:
15 set(refuse):
16 if refuse:
17 pending_peers.clear()
18
19
20class PendingPeer:
21 var connect_time: int
22 var tcp: StreamPeerTCP
23 var connection: StreamPeer
24 var ws: WebSocketPeer
25
26 func _init(p_tcp: StreamPeerTCP) -> void:
27 tcp = p_tcp
28 connection = p_tcp
29 connect_time = Time.get_ticks_msec()
30
31
32var tcp_server := TCPServer.new()
33var pending_peers: Array[PendingPeer] = []
34var peers: Dictionary
35
36
37func listen(port: int) -> int:
38 assert(not tcp_server.is_listening())
39 return tcp_server.listen(port)
40
41
42func stop() -> void:
43 tcp_server.stop()
44 pending_peers.clear()
45 peers.clear()
46
47
48func send(peer_id: int, message: String) -> int:
49 var type := typeof(message)
50 if peer_id <= 0:
51 # Send to multiple peers, (zero = broadcast, negative = exclude one).
52 for id: int in peers:
53 if id == -peer_id:
54 continue
55 if type == TYPE_STRING:
56 peers[id].send_text(message)
57 else:
58 peers[id].put_packet(message)
59 return OK
60
61 assert(peers.has(peer_id))
62 var socket: WebSocketPeer = peers[peer_id]
63 if type == TYPE_STRING:
64 return socket.send_text(message)
65 return socket.send(var_to_bytes(message))
66
67
68func get_message(peer_id: int) -> Variant:
69 assert(peers.has(peer_id))
70 var socket: WebSocketPeer = peers[peer_id]
71 if socket.get_available_packet_count() < 1:
72 return null
73 var pkt: PackedByteArray = socket.get_packet()
74 if socket.was_string_packet():
75 return pkt.get_string_from_utf8()
76 return bytes_to_var(pkt)
77
78
79func has_message(peer_id: int) -> bool:
80 assert(peers.has(peer_id))
81 return peers[peer_id].get_available_packet_count() > 0
82
83
84func _create_peer() -> WebSocketPeer:
85 var ws := WebSocketPeer.new()
86 ws.supported_protocols = supported_protocols
87 ws.handshake_headers = handshake_headers
88 return ws
89
90
91func poll() -> void:
92 if not tcp_server.is_listening():
93 return
94
95 while not refuse_new_connections and tcp_server.is_connection_available():
96 var conn: StreamPeerTCP = tcp_server.take_connection()
97 assert(conn != null)
98 pending_peers.append(PendingPeer.new(conn))
99
100 var to_remove := []
101
102 for p in pending_peers:
103 if not _connect_pending(p):
104 if p.connect_time + handshake_timout < Time.get_ticks_msec():
105 # Timeout.
106 to_remove.append(p)
107 continue # Still pending.
108
109 to_remove.append(p)
110
111 for r: RefCounted in to_remove:
112 pending_peers.erase(r)
113
114 to_remove.clear()
115
116 for id: int in peers:
117 var p: WebSocketPeer = peers[id]
118 p.poll()
119
120 if p.get_ready_state() != WebSocketPeer.STATE_OPEN:
121 client_disconnected.emit(id)
122 to_remove.append(id)
123 continue
124
125 while p.get_available_packet_count():
126 message_received.emit(id, get_message(id))
127
128 for r: int in to_remove:
129 peers.erase(r)
130 to_remove.clear()
131
132
133func _connect_pending(p: PendingPeer) -> bool:
134 if p.ws != null:
135 # Poll websocket client if doing handshake.
136 p.ws.poll()
137 var state := p.ws.get_ready_state()
138 if state == WebSocketPeer.STATE_OPEN:
139 var id := randi_range(2, 1 << 30)
140 peers[id] = p.ws
141 client_connected.emit(id)
142 return true # Success.
143 elif state != WebSocketPeer.STATE_CONNECTING:
144 return true # Failure.
145 return false # Still connecting.
146 elif p.tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED:
147 return true # TCP disconnected.
148 elif not use_tls:
149 # TCP is ready, create WS peer.
150 p.ws = _create_peer()
151 p.ws.accept_stream(p.tcp)
152 return false # WebSocketPeer connection is pending.
153
154 else:
155 if p.connection == p.tcp:
156 assert(tls_key != null and tls_cert != null)
157 var tls := StreamPeerTLS.new()
158 tls.accept_stream(p.tcp, TLSOptions.server(tls_key, tls_cert))
159 p.connection = tls
160 p.connection.poll()
161 var status: StreamPeerTLS.Status = p.connection.get_status()
162 if status == StreamPeerTLS.STATUS_CONNECTED:
163 p.ws = _create_peer()
164 p.ws.accept_stream(p.connection)
165 return false # WebSocketPeer connection is pending.
166 if status != StreamPeerTLS.STATUS_HANDSHAKING:
167 return true # Failure.
168
169 return false
170
171
172func _process(_delta: float) -> void:
173 poll()
diff --git a/apworld/client/victoryListener.gd b/apworld/client/victoryListener.gd new file mode 100644 index 0000000..e9089d7 --- /dev/null +++ b/apworld/client/victoryListener.gd
@@ -0,0 +1,20 @@
1extends Receiver
2
3
4func _ready():
5 super._ready()
6
7
8func handleTriggered():
9 triggered += 1
10 if triggered >= total:
11 var ap = global.get_node("Archipelago")
12 ap.client.completedGoal()
13
14 global.get_node("Messages").showMessage("You have completed your goal!")
15
16
17func handleUntriggered():
18 triggered -= 1
19 if triggered < total:
20 pass
diff --git a/apworld/client/visibilityListener.gd b/apworld/client/visibilityListener.gd new file mode 100644 index 0000000..5ea17a0 --- /dev/null +++ b/apworld/client/visibilityListener.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/listeners/visibilityListener.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
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/apworld/client/worldport.gd b/apworld/client/worldport.gd new file mode 100644 index 0000000..ed9891e --- /dev/null +++ b/apworld/client/worldport.gd
@@ -0,0 +1,61 @@
1extends "res://scripts/nodes/worldport.gd"
2
3var absolute_rotation = false
4var target_rotation = 0
5
6var port_id = null
7
8
9func _ready():
10 var node_path = String(
11 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
12 )
13
14 var ap = global.get_node("Archipelago")
15
16 if ap.shuffle_worldports:
17 var gamedata = global.get_node("Gamedata")
18 port_id = gamedata.get_port_for_map_node_path(global.map, node_path)
19 if port_id != null:
20 if port_id in ap.port_pairings:
21 var target_port = gamedata.objects.get_ports()[ap.port_pairings[port_id]]
22 var target_room = gamedata.objects.get_rooms()[target_port.get_room_id()]
23 var target_map = gamedata.objects.get_maps()[target_room.get_map_id()]
24
25 exit = target_map.get_name()
26 entry_point.x = target_port.get_destination().get_x()
27 entry_point.y = target_port.get_destination().get_y()
28 entry_point.z = target_port.get_destination().get_z()
29 absolute_rotation = true
30 target_rotation = target_port.get_rotation()
31 sets_entry_point = true
32 invisible = false
33 fades = true
34 else:
35 port_id = null
36
37 if global.map == "icarus" and exit == "daedalus":
38 if not ap.daedalus_roof_access:
39 entry_point = Vector3(58, 10, 0)
40
41 super._ready()
42
43
44func bodyEntered(body):
45 if body.is_in_group("player"):
46 if port_id != null:
47 var ap = global.get_node("Archipelago")
48 ap.client.checkWorldport(port_id)
49
50 if absolute_rotation:
51 entry_rotate.y = target_rotation - body.rotation_degrees.y
52
53 super.bodyEntered(body)
54
55
56func changeScene():
57 var player = get_tree().get_root().get_node("scene/player")
58 if player != null:
59 player.playable = false
60
61 super.changeScene()
diff --git a/apworld/client/worldportListener.gd b/apworld/client/worldportListener.gd new file mode 100644 index 0000000..4cff8e9 --- /dev/null +++ b/apworld/client/worldportListener.gd
@@ -0,0 +1,8 @@
1extends "res://scripts/nodes/listeners/worldportListener.gd"
2
3
4func handleTriggered():
5 if exit.begins_with("menus/credits"):
6 return
7
8 super.handleTriggered()