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/animationListener.gd38
-rw-r--r--apworld/client/apworld_runtime.gd44
-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.gd275
-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.gd46
-rw-r--r--apworld/client/effects.gd32
-rw-r--r--apworld/client/gamedata.gd294
-rw-r--r--apworld/client/keyHolder.gd38
-rw-r--r--apworld/client/keyHolderChecker.gd24
-rw-r--r--apworld/client/keyHolderResetterListener.gd8
-rw-r--r--apworld/client/keyboard.gd231
-rw-r--r--apworld/client/locationListener.gd20
-rw-r--r--apworld/client/main.gd296
-rw-r--r--apworld/client/manager.gd651
-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/panel.gd101
-rw-r--r--apworld/client/pauseMenu.gd91
-rw-r--r--apworld/client/player.gd349
-rw-r--r--apworld/client/rainbowText.gd10
-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.gd29
-rw-r--r--apworld/client/teleport.gd38
-rw-r--r--apworld/client/teleportListener.gd49
-rw-r--r--apworld/client/textclient.gd438
-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
40 files changed, 4036 insertions, 0 deletions
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..faf8e0c --- /dev/null +++ b/apworld/client/apworld_runtime.gd
@@ -0,0 +1,44 @@
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 load_script(path):
19 var true_path = _get_true_path(path)
20
21 var script = GDScript.new()
22 script.source_code = apworld_reader.read_file(true_path).get_string_from_utf8()
23 script.reload()
24
25 return script
26
27
28func read_path(path):
29 var true_path = _get_true_path(path)
30 return apworld_reader.read_file(true_path)
31
32
33func load_script_as_scene(path, scene_name):
34 var script = load_script(path)
35 var instance = script.new()
36 instance.name = scene_name
37
38 get_tree().unload_current_scene()
39 _load_scene.call_deferred(instance)
40
41
42func _load_scene(instance):
43 get_tree().get_root().add_child(instance)
44 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..9a4b402 --- /dev/null +++ b/apworld/client/client.gd
@@ -0,0 +1,275 @@
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
28
29signal could_not_connect
30signal connect_status
31signal client_connected(slot_data)
32signal item_received(item, amount)
33signal location_scout_received(location_id, item_name, player_name, flags, for_self)
34signal text_message_received(message)
35signal item_sent_notification(message)
36signal hint_received(message)
37signal accessible_locations_updated
38signal checked_locations_updated
39signal checked_worldports_updated
40signal keyboard_update_received
41
42
43func _init():
44 set_process_mode(Node.PROCESS_MODE_ALWAYS)
45
46 global._print("Instantiated APClient")
47
48
49func _ready():
50 _server = SCRIPT_websocketserver.new()
51 _server.client_connected.connect(_on_web_socket_server_client_connected)
52 _server.client_disconnected.connect(_on_web_socket_server_client_disconnected)
53 _server.message_received.connect(_on_web_socket_server_message_received)
54 add_child(_server)
55 _server.listen(43182)
56
57
58func _reset_state():
59 _should_process = false
60 _received_items = {}
61 _received_indexes = []
62 _checked_worldports = []
63 _accessible_locations = []
64 _accessible_worldports = []
65 _goal_accessible = false
66
67
68func disconnect_from_ap():
69 sendMessage([{"cmd": "Disconnect"}])
70
71
72func _on_web_socket_server_client_connected(peer_id: int) -> void:
73 var peer: WebSocketPeer = _server.peers[peer_id]
74 print("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()])
75 _server.send(-peer_id, "[%d] connected" % peer_id)
76
77
78func _on_web_socket_server_client_disconnected(peer_id: int) -> void:
79 var peer: WebSocketPeer = _server.peers[peer_id]
80 print(
81 (
82 "Remote client disconnected: %d. Code: %d, Reason: %s"
83 % [peer_id, peer.get_close_code(), peer.get_close_reason()]
84 )
85 )
86 _server.send(-peer_id, "[%d] disconnected" % peer_id)
87
88
89func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> void:
90 global._print("Got data from server: " + packet)
91 var json = JSON.new()
92 var jserror = json.parse(packet)
93 if jserror != OK:
94 global._print("Error parsing packet from AP: " + jserror.error_string)
95 return
96
97 for message in json.data:
98 var cmd = message["cmd"]
99 global._print("Received command: " + cmd)
100
101 if cmd == "Connected":
102 _seed = message["seed_name"]
103 _remote_version = message["version"]
104 _gen_version = message["generator_version"]
105 _team = message["team"]
106 _slot = message["slot"]
107 _slot_data = message["slot_data"]
108
109 _checked_locations = []
110 for location in message["checked_locations"]:
111 _checked_locations.append(int(location))
112
113 client_connected.emit(_slot_data)
114
115 elif cmd == "ConnectionRefused":
116 could_not_connect.emit(message["text"])
117 global._print("Connection to AP refused")
118
119 elif cmd == "UpdateLocations":
120 for location in message["locations"]:
121 var lint = int(location)
122 if not _checked_locations.has(lint):
123 _checked_locations.append(lint)
124
125 checked_locations_updated.emit()
126
127 elif cmd == "UpdateWorldports":
128 for port_id in message["worldports"]:
129 var lint = int(port_id)
130 if not _checked_worldports.has(lint):
131 _checked_worldports.append(lint)
132
133 checked_worldports_updated.emit()
134
135 elif cmd == "ItemReceived":
136 for item in message["items"]:
137 var index = int(item["index"])
138 if _received_indexes.has(index):
139 # Do not re-process items.
140 continue
141
142 _received_indexes.append(index)
143
144 var item_id = int(item["id"])
145 _received_items[item_id] = _received_items.get(item_id, 0) + 1
146
147 item_received.emit(item, _received_items[item_id])
148
149 elif cmd == "TextMessage":
150 text_message_received.emit(message["data"])
151
152 elif cmd == "ItemSentNotif":
153 item_sent_notification.emit(message)
154
155 elif cmd == "HintReceived":
156 hint_received.emit(message)
157
158 elif cmd == "LocationInfo":
159 for loc in message["locations"]:
160 location_scout_received.emit(
161 int(loc["id"]), loc["item"], loc["player"], int(loc["flags"]), int(loc["self"])
162 )
163
164 elif cmd == "AccessibleLocations":
165 _accessible_locations.clear()
166 _accessible_worldports.clear()
167
168 for loc in message["locations"]:
169 _accessible_locations.append(int(loc))
170
171 if "worldports" in message:
172 for port_id in message["worldports"]:
173 _accessible_worldports.append(int(port_id))
174
175 _goal_accessible = bool(message.get("goal", false))
176
177 accessible_locations_updated.emit()
178
179 elif cmd == "UpdateKeyboard":
180 var updates = {}
181 for k in message["updates"]:
182 updates[k] = int(message["updates"][k])
183
184 keyboard_update_received.emit(updates)
185
186 elif cmd == "PathReply":
187 var textclient = global.get_node("Textclient")
188 textclient.display_logical_path(
189 message["type"], int(message.get("id", null)), message["path"]
190 )
191
192
193func connectToServer(server, un, pw):
194 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
195
196 ap_server = server
197 ap_user = un
198 ap_pass = pw
199
200 _should_process = true
201
202 connect_status.emit("Connecting...")
203
204
205func sendMessage(msg):
206 var payload = JSON.stringify(msg)
207 _server.send(0, payload)
208
209
210func connectToRoom():
211 connect_status.emit("Authenticating...")
212
213 sendMessage(
214 [
215 {
216 "cmd": "Connect",
217 "password": ap_pass,
218 "game": "Lingo 2",
219 "name": ap_user,
220 }
221 ]
222 )
223
224
225func requestSync():
226 sendMessage([{"cmd": "Sync"}])
227
228
229func sendLocation(loc_id):
230 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
231
232
233func sendLocations(loc_ids):
234 sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}])
235
236
237func say(textdata):
238 sendMessage([{"cmd": "Say", "text": textdata}])
239
240
241func completedGoal():
242 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
243
244
245func scoutLocations(loc_ids):
246 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
247
248
249func updateKeyboard(updates):
250 sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}])
251
252
253func checkWorldport(port_id):
254 if not _checked_worldports.has(port_id):
255 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}])
256
257
258func getLogicalPath(object_type, object_id):
259 var msg = {"cmd": "GetPath", "type": object_type}
260 if object_id != null:
261 msg["id"] = object_id
262
263 sendMessage([msg])
264
265
266func sendQuit():
267 sendMessage([{"cmd": "Quit"}])
268
269
270func hasItem(item_id):
271 return _received_items.has(item_id)
272
273
274func getItemAmount(item_id):
275 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..49f5728 --- /dev/null +++ b/apworld/client/door.gd
@@ -0,0 +1,46 @@
1extends "res://scripts/nodes/door.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 if global.map == "the_sun_temple":
32 if name == "spe_EndPlatform" or name == "spe_entry_2":
33 senders = [NodePath("/root/scene/Panels/EndCheck_dog")]
34
35 if global.map == "the_parthenon":
36 if name == "spe_entry_1":
37 senders = [NodePath("/root/scene/Panels/EndCheck_dog")]
38
39 super._ready()
40
41
42func _readier():
43 var ap = global.get_node("Archipelago")
44
45 if ap.client.getItemAmount(item_id) >= item_amount:
46 handleTriggered()
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..9305003 --- /dev/null +++ b/apworld/client/gamedata.gd
@@ -0,0 +1,294 @@
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 ending_display_name_by_name = {}
18
19var kSYMBOL_ITEMS
20
21
22func _init(proto_script):
23 SCRIPT_proto = proto_script
24
25 kSYMBOL_ITEMS = {
26 SCRIPT_proto.PuzzleSymbol.SUN: "Sun Symbol",
27 SCRIPT_proto.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
28 SCRIPT_proto.PuzzleSymbol.ZERO: "Zero Symbol",
29 SCRIPT_proto.PuzzleSymbol.EXAMPLE: "Example Symbol",
30 SCRIPT_proto.PuzzleSymbol.BOXES: "Boxes Symbol",
31 SCRIPT_proto.PuzzleSymbol.PLANET: "Planet Symbol",
32 SCRIPT_proto.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
33 SCRIPT_proto.PuzzleSymbol.CROSS: "Cross Symbol",
34 SCRIPT_proto.PuzzleSymbol.SWEET: "Sweet Symbol",
35 SCRIPT_proto.PuzzleSymbol.GENDER: "Gender Symbol",
36 SCRIPT_proto.PuzzleSymbol.AGE: "Age Symbol",
37 SCRIPT_proto.PuzzleSymbol.SOUND: "Sound Symbol",
38 SCRIPT_proto.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
39 SCRIPT_proto.PuzzleSymbol.JOB: "Job Symbol",
40 SCRIPT_proto.PuzzleSymbol.STARS: "Stars Symbol",
41 SCRIPT_proto.PuzzleSymbol.NULL: "Null Symbol",
42 SCRIPT_proto.PuzzleSymbol.EVAL: "Eval Symbol",
43 SCRIPT_proto.PuzzleSymbol.LINGO: "Lingo Symbol",
44 SCRIPT_proto.PuzzleSymbol.QUESTION: "Question Symbol",
45 }
46
47
48func load(data_bytes):
49 objects = SCRIPT_proto.AllObjects.new()
50
51 var result_code = objects.from_bytes(data_bytes)
52 if result_code != SCRIPT_proto.PB_ERR.NO_ERRORS:
53 print("Could not load generated data: %d" % result_code)
54 return
55
56 for map in objects.get_maps():
57 map_id_by_name[map.get_name()] = map.get_id()
58
59 for door in objects.get_doors():
60 var map = objects.get_maps()[door.get_map_id()]
61
62 if not map.get_name() in door_id_by_map_node_path:
63 door_id_by_map_node_path[map.get_name()] = {}
64
65 var map_data = door_id_by_map_node_path[map.get_name()]
66 for receiver in door.get_receivers():
67 map_data[receiver] = door.get_id()
68
69 for painting_id in door.get_move_paintings():
70 var painting = objects.get_paintings()[painting_id]
71 map_data[painting.get_path()] = door.get_id()
72
73 if door.has_ap_id():
74 door_id_by_ap_id[door.get_ap_id()] = door.get_id()
75
76 if (
77 door.get_type() == SCRIPT_proto.DoorType.STANDARD
78 or door.get_type() == SCRIPT_proto.DoorType.LOCATION_ONLY
79 or door.get_type() == SCRIPT_proto.DoorType.GRAVESTONE
80 ):
81 location_name_by_id[door.get_ap_id()] = _get_door_location_name(door)
82
83 for painting in objects.get_paintings():
84 var room = objects.get_rooms()[painting.get_room_id()]
85 var map = objects.get_maps()[room.get_map_id()]
86
87 if not map.get_name() in painting_id_by_map_node_path:
88 painting_id_by_map_node_path[map.get_name()] = {}
89
90 var _map_data = painting_id_by_map_node_path[map.get_name()]
91
92 for port in objects.get_ports():
93 var room = objects.get_rooms()[port.get_room_id()]
94 var map = objects.get_maps()[room.get_map_id()]
95
96 if not map.get_name() in port_id_by_map_node_path:
97 port_id_by_map_node_path[map.get_name()] = {}
98
99 var map_data = port_id_by_map_node_path[map.get_name()]
100 map_data[port.get_path()] = port.get_id()
101
102 for progressive in objects.get_progressives():
103 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
104
105 for letter in objects.get_letters():
106 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
107 location_name_by_id[letter.get_ap_id()] = _get_letter_location_name(letter)
108
109 for mastery in objects.get_masteries():
110 location_name_by_id[mastery.get_ap_id()] = _get_mastery_location_name(mastery)
111
112 for ending in objects.get_endings():
113 var location_name = _get_ending_location_name(ending)
114 location_name_by_id[ending.get_ap_id()] = location_name
115 ending_display_name_by_name[ending.get_name()] = location_name
116
117 for keyholder in objects.get_keyholders():
118 if keyholder.has_key():
119 location_name_by_id[keyholder.get_ap_id()] = _get_keyholder_location_name(keyholder)
120
121 for panel in objects.get_panels():
122 var room = objects.get_rooms()[panel.get_room_id()]
123 var map = objects.get_maps()[room.get_map_id()]
124
125 if not map.get_name() in panel_id_by_map_node_path:
126 panel_id_by_map_node_path[map.get_name()] = {}
127
128 var map_data = panel_id_by_map_node_path[map.get_name()]
129 map_data[panel.get_path()] = panel.get_id()
130
131 for symbol_name in kSYMBOL_ITEMS.values():
132 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
133
134 for special_name in objects.get_special_ids().keys():
135 if special_name.begins_with("Anti "):
136 anti_trap_ids[objects.get_special_ids()[special_name]] = (
137 special_name.substr(5).to_lower()
138 )
139
140
141func get_door_for_map_node_path(map_name, node_path):
142 if not door_id_by_map_node_path.has(map_name):
143 return null
144
145 var map_data = door_id_by_map_node_path[map_name]
146 return map_data.get(node_path, null)
147
148
149func get_panel_for_map_node_path(map_name, node_path):
150 if not panel_id_by_map_node_path.has(map_name):
151 return null
152
153 var map_data = panel_id_by_map_node_path[map_name]
154 return map_data.get(node_path, null)
155
156
157func get_port_for_map_node_path(map_name, node_path):
158 if not port_id_by_map_node_path.has(map_name):
159 return null
160
161 var map_data = port_id_by_map_node_path[map_name]
162 return map_data.get(node_path, null)
163
164
165func get_door_ap_id(door_id):
166 var door = objects.get_doors()[door_id]
167 if door.has_ap_id():
168 return door.get_ap_id()
169 else:
170 return null
171
172
173func get_door_map_name(door_id):
174 var door = objects.get_doors()[door_id]
175 var map = objects.get_maps()[door.get_map_id()]
176 return map.get_name()
177
178
179func get_door_receivers(door_id):
180 var door = objects.get_doors()[door_id]
181 return door.get_receivers()
182
183
184func get_worldport_display_name(port_id):
185 var port = objects.get_ports()[port_id]
186 return "%s - %s" % [_get_room_object_map_name(port), port.get_display_name()]
187
188
189func _get_map_object_map_name(obj):
190 return objects.get_maps()[obj.get_map_id()].get_display_name()
191
192
193func _get_room_object_map_name(obj):
194 return _get_map_object_map_name(objects.get_rooms()[obj.get_room_id()])
195
196
197func _get_room_object_location_prefix(obj):
198 var room = objects.get_rooms()[obj.get_room_id()]
199 var game_map = objects.get_maps()[room.get_map_id()]
200
201 if room.has_panel_display_name():
202 return "%s (%s)" % [game_map.get_display_name(), room.get_panel_display_name()]
203 else:
204 return game_map.get_display_name()
205
206
207func _get_door_location_name(door):
208 var map_part = _get_room_object_location_prefix(door)
209
210 if door.has_location_name():
211 return "%s - %s" % [map_part, door.get_location_name()]
212
213 var generated_location_name = _get_generated_door_location_name(door)
214 if generated_location_name != null:
215 return generated_location_name
216
217 return "%s - %s" % [map_part, door.get_name()]
218
219
220func _get_generated_door_location_name(door):
221 if door.get_type() != SCRIPT_proto.DoorType.STANDARD:
222 return null
223
224 if door.get_keyholders().size() > 0 or door.get_endings().size() > 0 or door.has_complete_at():
225 return null
226
227 if door.get_panels().size() > 4:
228 return null
229
230 var map_areas = []
231 for panel_id in door.get_panels():
232 var panel = objects.get_panels()[panel_id.get_panel()]
233 var panel_room = objects.get_rooms()[panel.get_room_id()]
234 # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas.
235 var panel_display_name = ""
236 if panel_room.has_panel_display_name():
237 panel_display_name = panel_room.get_panel_display_name()
238 if not map_areas.has(panel_display_name):
239 map_areas.append(panel_display_name)
240
241 if map_areas.size() > 1:
242 return null
243
244 var game_map = objects.get_maps()[door.get_map_id()]
245 var map_area = map_areas[0]
246 var map_part
247 if map_area == "":
248 map_part = game_map.get_display_name()
249 else:
250 map_part = "%s (%s)" % [game_map.get_display_name(), map_area]
251
252 var panel_names = []
253 for panel_id in door.get_panels():
254 var panel_data = objects.get_panels()[panel_id.get_panel()]
255 var panel_name
256 if panel_data.has_display_name():
257 panel_name = panel_data.get_display_name()
258 else:
259 panel_name = panel_data.get_name()
260
261 var location_part
262 if panel_id.has_answer():
263 location_part = "%s/%s" % [panel_name, panel_id.get_answer().to_upper()]
264 else:
265 location_part = panel_name
266
267 panel_names.append(location_part)
268
269 panel_names.sort()
270
271 return map_part + " - " + ", ".join(panel_names)
272
273
274func _get_letter_location_name(letter):
275 var letter_level = 2 if (letter.has_level2() and letter.get_level2()) else 1
276 var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level]
277 return "%s - %s" % [_get_room_object_map_name(letter), letter_name]
278
279
280func _get_mastery_location_name(mastery):
281 return "%s - Mastery" % _get_room_object_map_name(mastery)
282
283
284func _get_ending_location_name(ending):
285 return (
286 "%s - %s Ending" % [_get_room_object_map_name(ending), ending.get_name().to_pascal_case()]
287 )
288
289
290func _get_keyholder_location_name(keyholder):
291 return (
292 "%s - %s Keyholder"
293 % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()]
294 )
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..d5300f3 --- /dev/null +++ b/apworld/client/keyHolderResetterListener.gd
@@ -0,0 +1,8 @@
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")
diff --git a/apworld/client/keyboard.gd b/apworld/client/keyboard.gd new file mode 100644 index 0000000..a59c4d0 --- /dev/null +++ b/apworld/client/keyboard.gd
@@ -0,0 +1,231 @@
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 if letters_in_keyholders.is_empty() and letters_blocked.is_empty():
195 return false
196
197 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty()
198
199 if keyholder_state.has(global.map):
200 for path in keyholder_state[global.map]:
201 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
202 keyholder_state[global.map][path], 0
203 )
204
205 keyholder_state.clear()
206 letters_in_keyholders.clear()
207 letters_blocked.clear()
208
209 update_unlocks()
210 save()
211
212 return cleared_anything
213
214
215func remote_keyboard_updated(updates):
216 var reverse = {}
217 var should_update = false
218
219 for k in updates:
220 if not letters_saved.has(k) or updates[k] > letters_saved[k]:
221 letters_saved[k] = updates[k]
222 should_update = true
223 elif updates[k] < letters_saved[k]:
224 reverse[k] = letters_saved[k]
225
226 if should_update:
227 update_unlocks()
228
229 if not reverse.is_empty():
230 var ap = global.get_node("Archipelago")
231 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..e1f9610 --- /dev/null +++ b/apworld/client/main.gd
@@ -0,0 +1,296 @@
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("animationListener.gd"))
40 installScriptExtension(runtime.load_script("collectable.gd"))
41 installScriptExtension(runtime.load_script("door.gd"))
42 installScriptExtension(runtime.load_script("keyHolder.gd"))
43 installScriptExtension(runtime.load_script("keyHolderChecker.gd"))
44 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd"))
45 installScriptExtension(runtime.load_script("painting.gd"))
46 installScriptExtension(runtime.load_script("panel.gd"))
47 installScriptExtension(runtime.load_script("pauseMenu.gd"))
48 installScriptExtension(runtime.load_script("player.gd"))
49 installScriptExtension(runtime.load_script("saver.gd"))
50 installScriptExtension(runtime.load_script("teleport.gd"))
51 installScriptExtension(runtime.load_script("teleportListener.gd"))
52 installScriptExtension(runtime.load_script("visibilityListener.gd"))
53 installScriptExtension(runtime.load_script("worldport.gd"))
54 installScriptExtension(runtime.load_script("worldportListener.gd"))
55
56 var proto_script = runtime.load_script("../generated/proto.gd")
57 var gamedata_script = runtime.load_script("gamedata.gd")
58 var gamedata_instance = gamedata_script.new(proto_script)
59 gamedata_instance.load(runtime.read_path("../generated/data.binpb"))
60 gamedata_instance.name = "Gamedata"
61 global.add_child(gamedata_instance)
62
63 var messages_script = runtime.load_script("messages.gd")
64 var messages_instance = messages_script.new()
65 messages_instance.name = "Messages"
66 messages_instance.SCRIPT_rainbowText = runtime.load_script("rainbowText.gd")
67 global.add_child(messages_instance)
68
69 var effects_script = runtime.load_script("effects.gd")
70 var effects_instance = effects_script.new()
71 effects_instance.name = "Effects"
72 global.add_child(effects_instance)
73
74 var textclient_script = runtime.load_script("textclient.gd")
75 var textclient_instance = textclient_script.new()
76 textclient_instance.name = "Textclient"
77 global.add_child(textclient_instance)
78
79 var compass_overlay_script = runtime.load_script("compass_overlay.gd")
80 var compass_overlay_instance = compass_overlay_script.new()
81 compass_overlay_instance.name = "Compass"
82 compass_overlay_instance.SCRIPT_compass = runtime.load_script("compass.gd")
83 global.add_child(compass_overlay_instance)
84
85 var ap = global.get_node("Archipelago")
86 var gamedata = global.get_node("Gamedata")
87 ap.ap_connected.connect(connectionSuccessful)
88 ap.could_not_connect.connect(connectionUnsuccessful)
89 ap.connect_status.connect(connectionStatus)
90
91 # Populate textboxes with AP settings.
92 get_node("../Panel/server_box").text = ap.ap_server
93 get_node("../Panel/player_box").text = ap.ap_user
94 get_node("../Panel/password_box").text = ap.ap_pass
95
96 var history_box = get_node("../Panel/connection_history")
97 if ap.connection_history.is_empty():
98 history_box.disabled = true
99 else:
100 history_box.disabled = false
101
102 var i = 0
103 for details in ap.connection_history:
104 history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i)
105 i += 1
106
107 history_box.get_popup().id_pressed.connect(historySelected)
108
109 # Show client version.
110 var version = gamedata.objects.get_version()
111 get_node("../Panel/title").text = (
112 "ARCHIPELAGO (%d.%d.%d)" % [version.get_major(), version.get_minor(), version.get_patch()]
113 )
114
115 # Increase font size in text boxes.
116 get_node("../Panel/server_box").add_theme_font_size_override("font_size", 36)
117 get_node("../Panel/player_box").add_theme_font_size_override("font_size", 36)
118 get_node("../Panel/password_box").add_theme_font_size_override("font_size", 36)
119
120 # Set up version mismatch dialog.
121 get_node("../Panel/VersionMismatch").confirmed.connect(startGame)
122 get_node("../Panel/VersionMismatch").get_cancel_button().pressed.connect(
123 versionMismatchDeclined
124 )
125
126 # Set up buttons.
127 get_node("../Panel/connect_button").pressed.connect(_connect_pressed)
128 get_node("../Panel/quit_button").pressed.connect(_back_pressed)
129
130
131func _connect_pressed():
132 get_node("../Panel/connect_button").disabled = true
133
134 var ap = global.get_node("Archipelago")
135 ap.ap_server = get_node("../Panel/server_box").text
136 ap.ap_user = get_node("../Panel/player_box").text
137 ap.ap_pass = get_node("../Panel/password_box").text
138 ap.saveSettings()
139
140 ap.connectToServer()
141
142
143func _back_pressed():
144 var ap = global.get_node("Archipelago")
145 ap.disconnect_from_ap()
146 ap.client.sendQuit()
147
148 get_tree().quit()
149
150
151# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
152func installScriptExtension(childScript: Resource):
153 # Force Godot to compile the script now.
154 # We need to do this here to ensure that the inheritance chain is
155 # properly set up, and multiple mods can chain-extend the same
156 # class multiple times.
157 # This is also needed to make Godot instantiate the extended class
158 # when creating singletons.
159 # The actual instance is thrown away.
160 childScript.new()
161
162 var parentScript = childScript.get_base_script()
163 var parentScriptPath = parentScript.resource_path
164 global._print("ModLoader: Installing script extension over %s" % parentScriptPath)
165 childScript.take_over_path(parentScriptPath)
166
167
168func connectionStatus(message):
169 var popup = get_node("../Panel/AcceptDialog")
170 popup.title = "Connecting to Archipelago"
171 popup.dialog_text = message
172 popup.exclusive = true
173 popup.get_ok_button().visible = false
174 popup.popup_centered()
175
176
177func connectionSuccessful():
178 var ap = global.get_node("Archipelago")
179 var gamedata = global.get_node("Gamedata")
180
181 # Check for major version mismatch.
182 if ap.apworld_version[0] != gamedata.objects.get_version().get_major():
183 get_node("../Panel/AcceptDialog").exclusive = false
184
185 var popup = get_node("../Panel/VersionMismatch")
186 popup.title = "Version Mismatch!"
187 popup.dialog_text = (
188 "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."
189 % [
190 ap.apworld_version[0],
191 ap.apworld_version[1],
192 ap.apworld_version[2],
193 gamedata.objects.get_version().get_major(),
194 gamedata.objects.get_version().get_minor(),
195 gamedata.objects.get_version().get_patch()
196 ]
197 )
198 popup.exclusive = true
199 popup.popup_centered()
200
201 return
202
203 startGame()
204
205
206func startGame():
207 var ap = global.get_node("Archipelago")
208
209 # Save connection details
210 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
211 if ap.connection_history.has(connection_details):
212 ap.connection_history.erase(connection_details)
213 ap.connection_history.push_front(connection_details)
214 if ap.connection_history.size() > 10:
215 ap.connection_history.resize(10)
216 ap.saveSettings()
217
218 # Switch to the_entry
219 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
220 global.user = ap.getSaveFileName()
221 global.universe = "lingo"
222 global.map = "the_entry"
223
224 unlocks.resetCollectables()
225 unlocks.resetData()
226
227 ap.setup_keys()
228
229 unlocks.loadCollectables()
230 unlocks.loadData()
231 unlocks.unlockKey("capslock", 1)
232
233 if ap.shuffle_worldports:
234 settings.worldport_fades = "default"
235 else:
236 settings.worldport_fades = "never"
237
238 clearResourceCache("res://objects/meshes/gridDoor.tscn")
239 clearResourceCache("res://objects/nodes/collectable.tscn")
240 clearResourceCache("res://objects/nodes/door.tscn")
241 clearResourceCache("res://objects/nodes/keyHolder.tscn")
242 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn")
243 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
244 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
245 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
246 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn")
247 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
248 clearResourceCache("res://objects/nodes/panel.tscn")
249 clearResourceCache("res://objects/nodes/player.tscn")
250 clearResourceCache("res://objects/nodes/saver.tscn")
251 clearResourceCache("res://objects/nodes/teleport.tscn")
252 clearResourceCache("res://objects/nodes/worldport.tscn")
253 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn")
254
255 var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
256 if paintings_dir:
257 paintings_dir.list_dir_begin()
258 var file_name = paintings_dir.get_next()
259 while file_name != "":
260 if not paintings_dir.current_is_dir() and file_name.ends_with(".tscn"):
261 clearResourceCache("res://objects/meshes/paintings/" + file_name)
262 file_name = paintings_dir.get_next()
263
264 switcher.switch_map.call_deferred("res://objects/scenes/the_entry.tscn")
265
266
267func connectionUnsuccessful(error_message):
268 get_node("../Panel/connect_button").disabled = false
269
270 var popup = get_node("../Panel/AcceptDialog")
271 popup.title = "Could not connect to Archipelago"
272 popup.dialog_text = error_message
273 popup.exclusive = true
274 popup.get_ok_button().visible = true
275 popup.popup_centered()
276
277
278func versionMismatchDeclined():
279 get_node("../Panel/AcceptDialog").hide()
280 get_node("../Panel/connect_button").disabled = false
281
282 var ap = global.get_node("Archipelago")
283 ap.disconnect_from_ap()
284
285
286func historySelected(index):
287 var ap = global.get_node("Archipelago")
288 var details = ap.connection_history[index]
289
290 get_node("../Panel/server_box").text = details[0]
291 get_node("../Panel/player_box").text = details[1]
292 get_node("../Panel/password_box").text = details[2]
293
294
295func clearResourceCache(path):
296 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..dac09b2 --- /dev/null +++ b/apworld/client/manager.gd
@@ -0,0 +1,651 @@
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
17
18var client
19var keyboard
20
21var _localdata_file = ""
22var _last_new_item = -1
23var _batch_locations = false
24var _held_locations = []
25var _held_location_scouts = []
26var _location_scouts = {}
27var _item_locks = {}
28var _inverse_item_locks = {}
29var _held_letters = {}
30var _letters_setup = false
31var _already_connected = false
32
33const kSHUFFLE_LETTERS_VANILLA = 0
34const kSHUFFLE_LETTERS_UNLOCKED = 1
35const kSHUFFLE_LETTERS_PROGRESSIVE = 2
36const kSHUFFLE_LETTERS_VANILLA_CYAN = 3
37const kSHUFFLE_LETTERS_ITEM_CYAN = 4
38
39const kLETTER_BEHAVIOR_VANILLA = 0
40const kLETTER_BEHAVIOR_ITEM = 1
41const kLETTER_BEHAVIOR_UNLOCKED = 2
42
43const kCYAN_DOOR_BEHAVIOR_H2 = 0
44const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
45const kCYAN_DOOR_BEHAVIOR_ITEM = 2
46
47const kEndingNameByVictoryValue = {
48 0: "GRAY",
49 1: "PURPLE",
50 2: "MINT",
51 3: "BLACK",
52 4: "BLUE",
53 5: "CYAN",
54 6: "RED",
55 7: "PLUM",
56 8: "ORANGE",
57 9: "GOLD",
58 10: "YELLOW",
59 11: "GREEN",
60 12: "WHITE",
61}
62
63var apworld_version = [0, 0, 0]
64var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
65var daedalus_roof_access = false
66var keyholder_sanity = false
67var port_pairings = {}
68var shuffle_control_center_colors = false
69var shuffle_doors = false
70var shuffle_gallery_paintings = false
71var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
72var shuffle_symbols = false
73var shuffle_worldports = false
74var strict_cyan_ending = false
75var strict_purple_ending = false
76var victory_condition = -1
77
78var color_by_material_path = {}
79
80signal could_not_connect
81signal connect_status
82signal ap_connected
83
84
85func _init():
86 # Read AP settings from file, if there are any
87 if FileAccess.file_exists("user://ap_settings"):
88 var file = FileAccess.open("user://ap_settings", FileAccess.READ)
89 var data = file.get_var(true)
90 file.close()
91
92 if typeof(data) != TYPE_ARRAY:
93 global._print("AP settings file is corrupted")
94 data = []
95
96 if data.size() > 0:
97 ap_server = data[0]
98
99 if data.size() > 1:
100 ap_user = data[1]
101
102 if data.size() > 2:
103 ap_pass = data[2]
104
105 if data.size() > 3:
106 connection_history = data[3]
107
108 if data.size() > 4:
109 show_compass = data[4]
110
111 if data.size() > 5:
112 show_locations = data[5]
113
114 if data.size() > 6:
115 show_minimap = data[6]
116
117 # We need to create a mapping from material paths to the original colors of
118 # those materials. We force reload the materials, overwriting any custom
119 # textures, and create the mapping. We then reload the textures in case the
120 # player had a custom one enabled.
121 var directory = DirAccess.open("res://assets/materials")
122 for material_name in directory.get_files():
123 var material = ResourceLoader.load(
124 "res://assets/materials/" + material_name, "", ResourceLoader.CACHE_MODE_REPLACE
125 )
126
127 color_by_material_path[material.resource_path] = Color(material.albedo_color)
128
129 settings.load_user_textures()
130
131
132func _ready():
133 client = SCRIPT_client.new()
134 client.SCRIPT_websocketserver = SCRIPT_websocketserver
135
136 client.item_received.connect(_process_item)
137 client.location_scout_received.connect(_process_location_scout)
138 client.text_message_received.connect(_process_text_message)
139 client.item_sent_notification.connect(_process_item_sent_notification)
140 client.hint_received.connect(_process_hint_received)
141 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
142 client.checked_locations_updated.connect(_on_checked_locations_updated)
143 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
144
145 client.could_not_connect.connect(_client_could_not_connect)
146 client.connect_status.connect(_client_connect_status)
147 client.client_connected.connect(_client_connected)
148
149 add_child(client)
150
151 keyboard = SCRIPT_keyboard.new()
152 add_child(keyboard)
153 client.keyboard_update_received.connect(keyboard.remote_keyboard_updated)
154
155
156func saveSettings():
157 # Save the AP settings to disk.
158 var path = "user://ap_settings"
159 var file = FileAccess.open(path, FileAccess.WRITE)
160
161 var data = [
162 ap_server,
163 ap_user,
164 ap_pass,
165 connection_history,
166 show_compass,
167 show_locations,
168 show_minimap,
169 ]
170 file.store_var(data, true)
171 file.close()
172
173
174func saveLocaldata():
175 # Save the MW/slot specific settings to disk.
176 var dir = DirAccess.open("user://")
177 var folder = "archipelago_data"
178 if not dir.dir_exists(folder):
179 dir.make_dir(folder)
180
181 var file = FileAccess.open(_localdata_file, FileAccess.WRITE)
182
183 var data = [
184 _last_new_item,
185 ]
186 file.store_var(data, true)
187 file.close()
188
189
190func connectToServer():
191 _last_new_item = -1
192 _batch_locations = false
193 _held_locations = []
194 _held_location_scouts = []
195 _location_scouts = {}
196 _letters_setup = false
197 _held_letters = {}
198 _already_connected = false
199
200 client.connectToServer(ap_server, ap_user, ap_pass)
201
202
203func getSaveFileName():
204 return "zzAP_%s_%d" % [client._seed, client._slot]
205
206
207func disconnect_from_ap():
208 _already_connected = false
209
210 var effects = global.get_node("Effects")
211 effects.set_connection_lost(false)
212
213 client.disconnect_from_ap()
214
215
216func get_item_id_for_door(door_id):
217 return _item_locks.get(door_id, null)
218
219
220func _process_item(item, amount):
221 var gamedata = global.get_node("Gamedata")
222
223 var item_id = int(item["id"])
224 var prog_id = null
225 if _inverse_item_locks.has(item_id):
226 for lock in _inverse_item_locks.get(item_id):
227 if lock[1] != amount:
228 continue
229
230 if gamedata.progressive_id_by_ap_id.has(item_id):
231 prog_id = lock[0]
232
233 if gamedata.get_door_map_name(lock[0]) != global.map:
234 continue
235
236 # TODO: fix doors opening from door groups
237 var receivers = gamedata.get_door_receivers(lock[0])
238 var scene = get_tree().get_root().get_node_or_null("scene")
239 if scene != null:
240 for receiver in receivers:
241 var rnode = scene.get_node_or_null(receiver)
242 if rnode != null:
243 rnode.handleTriggered()
244
245 var letter_id = gamedata.letter_id_by_ap_id.get(item_id, null)
246 if letter_id != null:
247 var letter = gamedata.objects.get_letters()[letter_id]
248 if not letter.has_level2() or not letter.get_level2():
249 _process_key_item(letter.get_key(), amount)
250
251 if gamedata.symbol_item_ids.has(item_id):
252 var player = get_tree().get_root().get_node_or_null("scene/player")
253 if player != null:
254 player.evaluate_solvability.emit()
255
256 if item_id == gamedata.objects.get_special_ids()["A Job Well Done"]:
257 update_job_well_done_sign()
258
259 # Show a message about the item if it's new.
260 if int(item["index"]) > _last_new_item:
261 _last_new_item = int(item["index"])
262 saveLocaldata()
263
264 var full_item_name = item["text"]
265 if prog_id != null:
266 var door = gamedata.objects.get_doors()[prog_id]
267 full_item_name = "%s (%s)" % [full_item_name, door.get_name()]
268
269 var message
270 if "sender" in item:
271 message = (
272 "Received %s from %s"
273 % [wrapInItemColorTags(full_item_name, item["flags"]), item["sender"]]
274 )
275 else:
276 message = "Found %s" % wrapInItemColorTags(full_item_name, item["flags"])
277
278 if gamedata.anti_trap_ids.has(item):
279 keyboard.block_letter(gamedata.anti_trap_ids[item])
280
281 global._print(message)
282
283 global.get_node("Messages").showMessage(message)
284
285
286func _process_item_sent_notification(message):
287 var sentMsg = (
288 "Sent %s to %s"
289 % [
290 wrapInItemColorTags(message["item_name"], message["item_flags"]),
291 message["receiver_name"]
292 ]
293 )
294 #if _hinted_locations.has(message["item"]["location"]):
295 # sentMsg += " ([color=#fafad2]Hinted![/color])"
296 global.get_node("Messages").showMessage(sentMsg)
297
298
299func _process_hint_received(message):
300 var is_for = ""
301 if message["self"] == 0:
302 is_for = " for %s" % message["receiver_name"]
303
304 global.get_node("Messages").showMessage(
305 (
306 "Hint: %s%s is on %s"
307 % [
308 wrapInItemColorTags(message["item_name"], message["item_flags"]),
309 is_for,
310 message["location_name"]
311 ]
312 )
313 )
314
315
316func _process_text_message(message):
317 var parts = []
318 for message_part in message:
319 if message_part["type"] == "text":
320 parts.append(message_part["text"])
321 elif message_part["type"] == "player":
322 if message_part["self"] == 1:
323 parts.append("[color=#ee00ee]%s[/color]" % message_part["text"])
324 else:
325 parts.append("[color=#fafad2]%s[/color]" % message_part["text"])
326 elif message_part["type"] == "item":
327 parts.append(wrapInItemColorTags(message_part["text"], int(message_part["flags"])))
328 elif message_part["type"] == "location":
329 parts.append("[color=#00ff7f]%s[/color]" % message_part["text"])
330
331 var textclient_node = global.get_node("Textclient")
332 if textclient_node != null:
333 textclient_node.parse_printjson("".join(parts))
334
335
336func _process_location_scout(location_id, item_name, player_name, flags, for_self):
337 _location_scouts[location_id] = {
338 "item": item_name, "player": player_name, "flags": flags, "for_self": for_self
339 }
340
341 if for_self and flags & 4 != 0:
342 # This is a trap for us, so let's not display it.
343 return
344
345 var gamedata = global.get_node("Gamedata")
346 var map_id = gamedata.map_id_by_name.get(global.map)
347
348 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
349 if letter_id != null:
350 var letter = gamedata.objects.get_letters()[letter_id]
351 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
352 if room.get_map_id() == map_id:
353 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
354 letter.get_path()
355 )
356 if collectable != null:
357 collectable.setScoutedText(item_name)
358
359
360func _on_accessible_locations_updated():
361 var textclient_node = global.get_node("Textclient")
362 if textclient_node != null:
363 textclient_node.update_locations()
364
365
366func _on_checked_locations_updated():
367 var textclient_node = global.get_node("Textclient")
368 if textclient_node != null:
369 textclient_node.update_locations(false)
370
371
372func _on_checked_worldports_updated():
373 var textclient_node = global.get_node("Textclient")
374 if textclient_node != null:
375 textclient_node.update_locations()
376 textclient_node.update_worldports()
377
378
379func _client_could_not_connect(message):
380 could_not_connect.emit(message)
381
382 if global.loaded:
383 var effects = global.get_node("Effects")
384 effects.set_connection_lost(true)
385
386 var messages = global.get_node("Messages")
387 messages.showMessage("Connection to multiworld lost.")
388
389
390func _client_connect_status(message):
391 connect_status.emit(message)
392
393
394func _client_connected(slot_data):
395 var effects = global.get_node("Effects")
396 effects.set_connection_lost(false)
397
398 if _already_connected:
399 var messages = global.get_node("Messages")
400 messages.showMessage("Reconnected to multiworld!")
401 return
402
403 _already_connected = true
404
405 var gamedata = global.get_node("Gamedata")
406
407 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
408 _last_new_item = -1
409
410 if FileAccess.file_exists(_localdata_file):
411 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
412 var localdata = []
413 if ap_file != null:
414 localdata = ap_file.get_var(true)
415 ap_file.close()
416
417 if typeof(localdata) != TYPE_ARRAY:
418 print("AP localdata file is corrupted")
419 localdata = []
420
421 if localdata.size() > 0:
422 _last_new_item = localdata[0]
423
424 # Read slot data.
425 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
426 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
427 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
428 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
429 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
430 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
431 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
432 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
433 shuffle_worldports = bool(slot_data.get("shuffle_worldports", false))
434 strict_cyan_ending = bool(slot_data.get("strict_cyan_ending", false))
435 strict_purple_ending = bool(slot_data.get("strict_purple_ending", false))
436 victory_condition = int(slot_data.get("victory_condition", 0))
437
438 if slot_data.has("version"):
439 var version_msg = slot_data["version"]
440 apworld_version = [int(version_msg[0]), int(version_msg[1]), 0]
441 if version_msg.size() > 2:
442 apworld_version[2] = int(version_msg[2])
443
444 port_pairings.clear()
445 if slot_data.has("port_pairings"):
446 var raw_pp = slot_data.get("port_pairings")
447
448 for p1 in raw_pp.keys():
449 port_pairings[int(p1)] = int(raw_pp[p1])
450
451 # Set up item locks.
452 _item_locks = {}
453
454 if shuffle_doors:
455 for door in gamedata.objects.get_doors():
456 if (
457 door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD
458 or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
459 ):
460 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
461
462 for progressive in gamedata.objects.get_progressives():
463 for i in range(0, progressive.get_doors().size()):
464 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
465 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
466
467 for door_group in gamedata.objects.get_door_groups():
468 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR:
469 if shuffle_worldports:
470 continue
471 elif door_group.get_type() != gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP:
472 continue
473
474 for door in door_group.get_doors():
475 _item_locks[door] = [door_group.get_ap_id(), 1]
476
477 if shuffle_control_center_colors:
478 for door in gamedata.objects.get_doors():
479 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
480 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
481
482 for door_group in gamedata.objects.get_door_groups():
483 if (
484 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR
485 and not shuffle_worldports
486 ):
487 for door in door_group.get_doors():
488 _item_locks[door] = [door_group.get_ap_id(), 1]
489
490 if shuffle_gallery_paintings:
491 for door in gamedata.objects.get_doors():
492 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
493 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
494
495 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
496 for door_group in gamedata.objects.get_door_groups():
497 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
498 for door in door_group.get_doors():
499 if not _item_locks.has(door):
500 _item_locks[door] = [door_group.get_ap_id(), 1]
501
502 # Create a reverse item locks map for processing items.
503 _inverse_item_locks = {}
504
505 for door_id in _item_locks.keys():
506 var lock = _item_locks.get(door_id)
507
508 if not _inverse_item_locks.has(lock[0]):
509 _inverse_item_locks[lock[0]] = []
510
511 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
512
513 if shuffle_worldports:
514 var textclient = global.get_node("Textclient")
515 textclient.setup_worldports()
516
517 ap_connected.emit()
518
519
520func start_batching_locations():
521 _batch_locations = true
522
523
524func send_location(loc_id):
525 if client._checked_locations.has(loc_id):
526 return
527
528 if _batch_locations:
529 _held_locations.append(loc_id)
530 else:
531 client.sendLocation(loc_id)
532
533
534func scout_location(loc_id):
535 if _location_scouts.has(loc_id):
536 return _location_scouts.get(loc_id)
537
538 if _batch_locations:
539 _held_location_scouts.append(loc_id)
540 else:
541 client.scoutLocation(loc_id)
542
543 return null
544
545
546func stop_batching_locations():
547 _batch_locations = false
548
549 if not _held_locations.is_empty():
550 client.sendLocations(_held_locations)
551 _held_locations.clear()
552
553 if not _held_location_scouts.is_empty():
554 client.scoutLocations(_held_location_scouts)
555 _held_location_scouts.clear()
556
557
558func colorForItemType(flags):
559 var int_flags = int(flags)
560 if int_flags & 1: # progression
561 if int_flags & 2: # proguseful
562 return "#f0d200"
563 else:
564 return "#bc51e0"
565 elif int_flags & 2: # useful
566 return "#2b67ff"
567 elif int_flags & 4: # trap
568 return "#d63a22"
569 else: # filler
570 return "#14de9e"
571
572
573func wrapInItemColorTags(text, flags):
574 var int_flags = int(flags)
575 if int_flags & 1 and int_flags & 2: # proguseful
576 return "[rainbow]%s[/rainbow]" % text
577 else:
578 return "[color=%s]%s[/color]" % [colorForItemType(flags), text]
579
580
581func get_letter_behavior(key, level2):
582 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
583 return kLETTER_BEHAVIOR_UNLOCKED
584
585 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
586 if level2:
587 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
588 return kLETTER_BEHAVIOR_VANILLA
589 else:
590 return kLETTER_BEHAVIOR_ITEM
591 else:
592 return kLETTER_BEHAVIOR_UNLOCKED
593
594 if not level2 and ["h", "i", "n", "t"].has(key):
595 # This differs from the equivalent function in the apworld. Logically it is
596 # the same as UNLOCKED since they are in the starting room, but VANILLA
597 # means the player still has to actually pick up the letters.
598 return kLETTER_BEHAVIOR_VANILLA
599
600 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
601 return kLETTER_BEHAVIOR_ITEM
602
603 return kLETTER_BEHAVIOR_VANILLA
604
605
606func setup_keys():
607 keyboard.load_seed()
608
609 _letters_setup = true
610
611 for k in _held_letters.keys():
612 _process_key_item(k, _held_letters[k])
613
614 _held_letters.clear()
615
616
617func _process_key_item(key, level):
618 if not _letters_setup:
619 _held_letters[key] = max(_held_letters.get(key, 0), level)
620 return
621
622 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
623 level += 1
624
625 keyboard.collect_remote_letter(key, level)
626
627
628func update_job_well_done_sign():
629 if global.map != "daedalus":
630 return
631
632 var gamedata = global.get_node("Gamedata")
633 var job_item = gamedata.objects.get_special_ids()["A Job Well Done"]
634 var jobs_done = client.getItemAmount(job_item)
635
636 var sign2 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign2")
637 var sign3 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign3")
638
639 if sign2 != null and sign3 != null:
640 if jobs_done == 0:
641 sign2.text = "what are you doing"
642 sign3.text = "?"
643 elif jobs_done == 1:
644 sign2.text = "a job well done"
645 sign3.text = "is its own reward"
646 else:
647 sign2.text = "%d jobs well done" % jobs_done
648 sign3.text = "are their own reward"
649
650 sign2.get_node("MeshInstance3D").mesh.text = sign2.text
651 sign3.get_node("MeshInstance3D").mesh.text = sign3.text
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/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..72b45e8 --- /dev/null +++ b/apworld/client/pauseMenu.gd
@@ -0,0 +1,91 @@
1extends "res://scripts/ui/pauseMenu.gd"
2
3var compass_button
4var locations_button
5var minimap_button
6
7
8func _ready():
9 var ap_panel = Panel.new()
10 ap_panel.name = "Archipelago"
11 get_node("menu/settings/settingsInner/TabContainer").add_child(ap_panel)
12
13 var ap = global.get_node("Archipelago")
14
15 compass_button = CheckBox.new()
16 compass_button.text = "show compass"
17 compass_button.button_pressed = ap.show_compass
18 compass_button.position = Vector2(65, 100)
19 compass_button.theme = preload("res://assets/themes/baseUI.tres")
20 compass_button.add_theme_font_size_override("font_size", 60)
21 compass_button.pressed.connect(_toggle_compass)
22 ap_panel.add_child(compass_button)
23
24 locations_button = CheckBox.new()
25 locations_button.text = "show locations overlay"
26 locations_button.button_pressed = ap.show_locations
27 locations_button.position = Vector2(65, 200)
28 locations_button.theme = preload("res://assets/themes/baseUI.tres")
29 locations_button.add_theme_font_size_override("font_size", 60)
30 locations_button.pressed.connect(_toggle_locations)
31 ap_panel.add_child(locations_button)
32
33 minimap_button = CheckBox.new()
34 minimap_button.text = "show minimap"
35 minimap_button.button_pressed = ap.show_minimap
36 minimap_button.position = Vector2(65, 300)
37 minimap_button.theme = preload("res://assets/themes/baseUI.tres")
38 minimap_button.add_theme_font_size_override("font_size", 60)
39 minimap_button.pressed.connect(_toggle_minimap)
40 ap_panel.add_child(minimap_button)
41
42 super._ready()
43
44
45func _pause_game():
46 global.get_node("Textclient").dismiss()
47 super._pause_game()
48
49
50func _main_menu():
51 global.loaded = false
52 global.get_node("Archipelago").disconnect_from_ap()
53 global.get_node("Messages").clear()
54 global.get_node("Compass").visible = false
55 global.get_node("Textclient").reset()
56
57 autosplitter.reset()
58 _unpause_game()
59 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
60 musicPlayer.stop()
61
62 var runtime = global.get_node("Runtime")
63 runtime.load_script_as_scene.call_deferred("settings_screen.gd", "settings_screen")
64
65
66func _toggle_compass():
67 var ap = global.get_node("Archipelago")
68 ap.show_compass = compass_button.button_pressed
69 ap.saveSettings()
70
71 var compass = global.get_node("Compass")
72 compass.visible = compass_button.button_pressed
73
74
75func _toggle_locations():
76 var ap = global.get_node("Archipelago")
77 ap.show_locations = locations_button.button_pressed
78 ap.saveSettings()
79
80 var textclient = global.get_node("Textclient")
81 textclient.update_locations_visibility()
82
83
84func _toggle_minimap():
85 var ap = global.get_node("Archipelago")
86 ap.show_minimap = minimap_button.button_pressed
87 ap.saveSettings()
88
89 var minimap = get_tree().get_root().get_node("scene/Minimap")
90 if minimap != null:
91 minimap.visible = ap.show_minimap
diff --git a/apworld/client/player.gd b/apworld/client/player.gd new file mode 100644 index 0000000..b73f61e --- /dev/null +++ b/apworld/client/player.gd
@@ -0,0 +1,349 @@
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
17 compass = global.get_node("Compass")
18 compass.visible = ap.show_compass
19
20 ap.start_batching_locations()
21
22 # Set up door locations.
23 var map_id = gamedata.map_id_by_name.get(global.map)
24 for door in gamedata.objects.get_doors():
25 if door.get_map_id() != map_id:
26 continue
27
28 if not door.has_ap_id():
29 continue
30
31 if (
32 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
33 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
34 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
35 ):
36 continue
37
38 var locationListener = ap.SCRIPT_locationListener.new()
39 locationListener.location_id = door.get_ap_id()
40 locationListener.name = "locationListener_%d" % door.get_ap_id()
41
42 for panel_ref in door.get_panels():
43 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
44 var panel_path = panel_data.get_path()
45
46 if panel_ref.has_answer():
47 for proxy in panel_data.get_proxies():
48 if proxy.get_answer() == panel_ref.get_answer():
49 panel_path = proxy.get_path()
50 break
51
52 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
53
54 for keyholder_ref in door.get_keyholders():
55 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
56
57 var khl = khl_script.new()
58 khl.name = (
59 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
60 )
61 khl.answer = keyholder_ref.get_key()
62 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
63 get_parent().add_child.call_deferred(khl)
64
65 locationListener.senders.append(NodePath("../" + khl.name))
66
67 for sender in door.get_senders():
68 locationListener.senders.append(NodePath("/root/scene/" + sender))
69
70 if door.has_complete_at():
71 locationListener.complete_at = door.get_complete_at()
72
73 get_parent().add_child.call_deferred(locationListener)
74
75 # Set up letter locations.
76 for letter in gamedata.objects.get_letters():
77 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
78 if room.get_map_id() != map_id:
79 continue
80
81 var locationListener = ap.SCRIPT_locationListener.new()
82 locationListener.location_id = letter.get_ap_id()
83 locationListener.name = "locationListener_%d" % letter.get_ap_id()
84 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
85
86 get_parent().add_child.call_deferred(locationListener)
87
88 if (
89 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
90 != ap.kLETTER_BEHAVIOR_VANILLA
91 ):
92 var scout = ap.scout_location(letter.get_ap_id())
93 if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0):
94 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
95 letter.get_path()
96 )
97 if collectable != null:
98 collectable.setScoutedText.call_deferred(scout["item"])
99
100 # Set up mastery locations.
101 for mastery in gamedata.objects.get_masteries():
102 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
103 if room.get_map_id() != map_id:
104 continue
105
106 var locationListener = ap.SCRIPT_locationListener.new()
107 locationListener.location_id = mastery.get_ap_id()
108 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
109 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
110
111 get_parent().add_child.call_deferred(locationListener)
112
113 # Set up ending locations.
114 for ending in gamedata.objects.get_endings():
115 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
116 if room.get_map_id() != map_id:
117 continue
118
119 var locationListener = ap.SCRIPT_locationListener.new()
120 locationListener.location_id = ending.get_ap_id()
121 locationListener.name = "locationListener_%d" % ending.get_ap_id()
122 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
123
124 get_parent().add_child.call_deferred(locationListener)
125
126 if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
127 var victoryListener = ap.SCRIPT_victoryListener.new()
128 victoryListener.name = "victoryListener"
129 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
130
131 get_parent().add_child.call_deferred(victoryListener)
132
133 # Set up keyholder locations, in keyholder sanity.
134 if ap.keyholder_sanity:
135 for keyholder in gamedata.objects.get_keyholders():
136 if not keyholder.has_key():
137 continue
138
139 var room = gamedata.objects.get_rooms()[keyholder.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 = keyholder.get_ap_id()
145 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
146
147 var khl = khl_script.new()
148 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
149 khl.answer = keyholder.get_key()
150 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
151 get_parent().add_child.call_deferred(khl)
152
153 locationListener.senders.append(NodePath("../" + khl.name))
154
155 get_parent().add_child.call_deferred(locationListener)
156
157 # Block off roof access in Daedalus.
158 if global.map == "daedalus" and not ap.daedalus_roof_access:
159 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
160 _set_up_invis_wall(51.5, 11, -17, 16, 10, 1)
161 _set_up_invis_wall(46, 10, -9.5, 1, 10, 10)
162 _set_up_invis_wall(67.5, 11, 17, 16, 10, 1)
163 _set_up_invis_wall(50.5, 11, 14, 10, 10, 1)
164 _set_up_invis_wall(39, 10, 18.5, 1, 10, 22)
165 _set_up_invis_wall(20, 15, 18.5, 1, 10, 16)
166 _set_up_invis_wall(11.5, 15, 3, 32, 10, 1)
167 _set_up_invis_wall(11.5, 16, -20, 14, 20, 1)
168 _set_up_invis_wall(14, 16, -26.5, 1, 20, 4)
169 _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25)
170 _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1)
171 _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1)
172 _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11)
173 _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11)
174
175 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
176 var warp_exit = warp_exit_prefab.instantiate()
177 warp_exit.name = "roof_access_blocker_warp_exit"
178 warp_exit.position = Vector3(58, 10, 0)
179 warp_exit.rotation_degrees.y = 90
180 get_parent().add_child.call_deferred(warp_exit)
181
182 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
183 var warp_enter = warp_enter_prefab.instantiate()
184 warp_enter.target = warp_exit
185 warp_enter.position = Vector3(76.5, 30, 1)
186 warp_enter.scale = Vector3(4, 1.5, 1)
187 warp_enter.rotation_degrees.y = 90
188 get_parent().add_child.call_deferred(warp_enter)
189
190 if global.map == "the_entry":
191 # Remove door behind X1.
192 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
193 door_node.handleTriggered()
194
195 # Display win condition.
196 var sign_prefab = preload("res://objects/nodes/sign.tscn")
197 var sign1 = sign_prefab.instantiate()
198 sign1.position = Vector3(-7, 5, -15.01)
199 sign1.text = "victory"
200 get_parent().add_child.call_deferred(sign1)
201
202 var sign2 = sign_prefab.instantiate()
203 sign2.position = Vector3(-7, 4, -15.01)
204 sign2.text = "%s ending" % ap.kEndingNameByVictoryValue.get(ap.victory_condition, "?")
205
206 var sign2_color = ap.kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
207 if sign2_color == "white":
208 sign2_color = "silver"
209
210 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
211 get_parent().add_child.call_deferred(sign2)
212
213 # Add the strict purple ending validation.
214 if global.map == "the_sun_temple" and ap.strict_purple_ending:
215 var panel_prefab = preload("res://objects/nodes/panel.tscn")
216 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
217 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
218
219 var previous_panel = null
220 var next_y = -100
221 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
222 for word in words:
223 var panel = panel_prefab.instantiate()
224 panel.position = Vector3(0, next_y, 0)
225 next_y -= 10
226 panel.clue = word
227 panel.symbol = ""
228 panel.answer = word
229 panel.name = "EndCheck_%s" % word
230
231 var tpl = tpl_prefab.instantiate()
232 tpl.teleport_point = Vector3(0, 1, 0)
233 tpl.teleport_rotate = Vector3(-45, 180, 0)
234 tpl.target_path = panel
235 tpl.name = "Teleport"
236
237 if previous_panel == null:
238 tpl.senders.append(NodePath("/root/scene/Panels/End/panel_24"))
239 else:
240 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
241
242 var reversing = reverse_prefab.instantiate()
243 reversing.senders.append(NodePath(".."))
244 reversing.name = "Reversing"
245 tpl.senders.append(NodePath("../Reversing"))
246
247 panel.add_child.call_deferred(tpl)
248 panel.add_child.call_deferred(reversing)
249 get_parent().get_node("Panels").add_child.call_deferred(panel)
250
251 previous_panel = panel
252
253 # Duplicate the doors that usually wait on EQUINOX. We can't set the senders
254 # here for some reason so we actually set them in the door ready function.
255 var endplat = get_node("/root/scene/Components/Doors/EndPlatform")
256 var endplat2 = endplat.duplicate()
257 endplat2.name = "spe_EndPlatform"
258 endplat.get_parent().add_child.call_deferred(endplat2)
259 endplat.queue_free()
260
261 var entry2 = get_node("/root/scene/Components/Doors/entry_2")
262 var entry22 = entry2.duplicate()
263 entry22.name = "spe_entry_2"
264 entry2.get_parent().add_child.call_deferred(entry22)
265 entry2.queue_free()
266
267 # Add the strict cyan ending validation.
268 if global.map == "the_parthenon" and ap.strict_cyan_ending:
269 var panel_prefab = preload("res://objects/nodes/panel.tscn")
270 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
271 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
272
273 var previous_panel = null
274 var next_y = -100
275 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
276 for word in words:
277 var panel = panel_prefab.instantiate()
278 panel.position = Vector3(0, next_y, 0)
279 next_y -= 10
280 panel.clue = word
281 panel.symbol = "."
282 panel.answer = "%s%s" % [word, word]
283 panel.name = "EndCheck_%s" % word
284
285 var tpl = tpl_prefab.instantiate()
286 tpl.teleport_point = Vector3(0, 1, -11)
287 tpl.teleport_rotate = Vector3(-45, 0, 0)
288 tpl.target_path = panel
289 tpl.name = "Teleport"
290
291 if previous_panel == null:
292 tpl.senderGroup.append(NodePath("/root/scene/Panels/Rulers"))
293 else:
294 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
295
296 var reversing = reverse_prefab.instantiate()
297 reversing.senders.append(NodePath(".."))
298 reversing.name = "Reversing"
299 tpl.senders.append(NodePath("../Reversing"))
300
301 panel.add_child.call_deferred(tpl)
302 panel.add_child.call_deferred(reversing)
303 get_parent().get_node("Panels").add_child.call_deferred(panel)
304
305 previous_panel = panel
306
307 # Duplicate the door that usually waits on the rulers. We can't set the
308 # senders here for some reason so we actually set them in the door ready
309 # function.
310 var entry1 = get_node("/root/scene/Components/Doors/entry_1")
311 var entry12 = entry1.duplicate()
312 entry12.name = "spe_entry_1"
313 entry1.get_parent().add_child.call_deferred(entry12)
314 entry1.queue_free()
315
316 ap.update_job_well_done_sign()
317
318 var minimap = ap.SCRIPT_minimap.new()
319 minimap.name = "Minimap"
320 minimap.visible = ap.show_minimap
321 get_parent().add_child.call_deferred(minimap)
322
323 super._ready()
324
325 await get_tree().process_frame
326 await get_tree().process_frame
327
328 ap.stop_batching_locations()
329
330
331func _set_up_invis_wall(x, y, z, sx, sy, sz):
332 var prefab = preload("res://objects/nodes/block.tscn")
333 var newwall = prefab.instantiate()
334 newwall.position.x = x
335 newwall.position.y = y
336 newwall.position.z = z
337 newwall.scale.x = sz
338 newwall.scale.y = sy
339 newwall.scale.z = sx
340 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
341 newwall.visibility_range_end = 3
342 newwall.visibility_range_end_margin = 1
343 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
344 newwall.skeleton = ".."
345 get_parent().add_child.call_deferred(newwall)
346
347
348func _process(_dt):
349 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/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..35428ea --- /dev/null +++ b/apworld/client/source_runtime.gd
@@ -0,0 +1,29 @@
1extends Node
2
3var source_path
4
5
6func _init(path):
7 source_path = path
8
9
10func load_script(path):
11 return ResourceLoader.load("%s/%s" % [source_path, path])
12
13
14func read_path(path):
15 return FileAccess.get_file_as_bytes("%s/%s" % [source_path, path])
16
17
18func load_script_as_scene(path, scene_name):
19 var script = load_script(path)
20 var instance = script.new()
21 instance.name = scene_name
22
23 get_tree().unload_current_scene()
24 _load_scene.call_deferred(instance)
25
26
27func _load_scene(instance):
28 get_tree().get_root().add_child(instance)
29 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..f785a03 --- /dev/null +++ b/apworld/client/textclient.gd
@@ -0,0 +1,438 @@
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 = {}
19
20var worldports_tab
21var worldports_tree
22var port_tree_item_by_map = {}
23var port_tree_item_by_map_port = {}
24
25const kLocation = 0
26const kWorldport = 1
27const kGoal = 2
28
29
30func _ready():
31 process_mode = ProcessMode.PROCESS_MODE_ALWAYS
32 layer = 2
33
34 locations_overlay = RichTextLabel.new()
35 locations_overlay.name = "LocationsOverlay"
36 locations_overlay.offset_top = 220
37 locations_overlay.offset_bottom = 720
38 locations_overlay.offset_left = 20
39 locations_overlay.anchor_right = 1.0
40 locations_overlay.offset_right = -10
41 locations_overlay.scroll_active = false
42 locations_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
43 locations_overlay.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
44 add_child(locations_overlay)
45 update_locations_visibility()
46
47 tabs = TabContainer.new()
48 tabs.name = "Tabs"
49 tabs.offset_left = 100
50 tabs.offset_right = 1820
51 tabs.offset_top = 100
52 tabs.offset_bottom = 980
53 tabs.visible = false
54 tabs.theme = preload("res://assets/themes/baseUI.tres")
55 tabs.add_theme_font_size_override("font_size", 36)
56 add_child(tabs)
57
58 panel = MarginContainer.new()
59 panel.name = "Text Client"
60 panel.add_theme_constant_override("margin_top", 60)
61 panel.add_theme_constant_override("margin_left", 60)
62 panel.add_theme_constant_override("margin_right", 60)
63 panel.add_theme_constant_override("margin_bottom", 60)
64 tabs.add_child(panel)
65
66 label = RichTextLabel.new()
67 label.set_name("Label")
68 label.scroll_following = true
69 label.selection_enabled = true
70 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
71 label.size_flags_vertical = Control.SIZE_EXPAND_FILL
72 label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
73 label.push_font_size(30)
74
75 var entry_style = StyleBoxFlat.new()
76 entry_style.bg_color = Color(0.9, 0.9, 0.9, 1)
77
78 entry = LineEdit.new()
79 entry.set_name("Entry")
80 entry.add_theme_font_override("font", preload("res://assets/fonts/Lingo2.ttf"))
81 entry.add_theme_font_size_override("font_size", 36)
82 entry.add_theme_color_override("font_color", Color(0, 0, 0, 1))
83 entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1))
84 entry.add_theme_stylebox_override("focus", entry_style)
85 entry.text_submitted.connect(text_entered)
86
87 var tc_arranger = VBoxContainer.new()
88 tc_arranger.add_child(label)
89 tc_arranger.add_child(entry)
90 tc_arranger.add_theme_constant_override("separation", 40)
91 panel.add_child(tc_arranger)
92
93 var tracker_margins = MarginContainer.new()
94 tracker_margins.name = "Locations"
95 tracker_margins.add_theme_constant_override("margin_top", 60)
96 tracker_margins.add_theme_constant_override("margin_left", 60)
97 tracker_margins.add_theme_constant_override("margin_right", 60)
98 tracker_margins.add_theme_constant_override("margin_bottom", 60)
99 tabs.add_child(tracker_margins)
100
101 tracker_tree = Tree.new()
102 tracker_tree.columns = 3
103 tracker_tree.hide_root = true
104 tracker_tree.add_theme_font_size_override("font_size", 24)
105 tracker_tree.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 1))
106 tracker_tree.add_theme_constant_override("v_separation", 1)
107 tracker_tree.item_edited.connect(_on_tracker_button_clicked)
108 tracker_tree.set_column_expand(0, false)
109 tracker_tree.set_column_expand(1, true)
110 tracker_tree.set_column_expand(2, false)
111 tracker_tree.set_column_custom_minimum_width(2, 200)
112 tracker_margins.add_child(tracker_tree)
113
114 worldports_tab = MarginContainer.new()
115 worldports_tab.name = "Worldports"
116 worldports_tab.add_theme_constant_override("margin_top", 60)
117 worldports_tab.add_theme_constant_override("margin_left", 60)
118 worldports_tab.add_theme_constant_override("margin_right", 60)
119 worldports_tab.add_theme_constant_override("margin_bottom", 60)
120 tabs.add_child(worldports_tab)
121 tabs.set_tab_hidden(2, true)
122
123 worldports_tree = Tree.new()
124 worldports_tree.columns = 2
125 worldports_tree.hide_root = true
126 worldports_tree.theme = preload("res://assets/themes/baseUI.tres")
127 worldports_tree.add_theme_font_size_override("font_size", 24)
128 worldports_tab.add_child(worldports_tree)
129
130 var runtime = global.get_node("Runtime")
131 var location_image = Image.new()
132 location_image.load_png_from_buffer(runtime.read_path("assets/location.png"))
133 location_texture = ImageTexture.create_from_image(location_image)
134
135 var worldport_image = Image.new()
136 worldport_image.load_png_from_buffer(runtime.read_path("assets/worldport.png"))
137 worldport_texture = ImageTexture.create_from_image(worldport_image)
138
139 var goal_image = Image.new()
140 goal_image.load_png_from_buffer(runtime.read_path("assets/goal.png"))
141 goal_texture = ImageTexture.create_from_image(goal_image)
142
143
144func _input(event):
145 if global.loaded and event is InputEventKey and event.pressed:
146 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
147 if !get_tree().paused:
148 is_open = true
149 get_tree().paused = true
150 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
151 tabs.visible = true
152 entry.grab_focus()
153 get_viewport().set_input_as_handled()
154 else:
155 dismiss()
156 elif event.keycode == KEY_ESCAPE:
157 if is_open:
158 dismiss()
159 get_viewport().set_input_as_handled()
160
161
162func dismiss():
163 if is_open:
164 get_tree().paused = false
165 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
166 tabs.visible = false
167 is_open = false
168
169
170func parse_printjson(text):
171 label.append_text("[p]" + text + "[/p]")
172
173
174func text_entered(text):
175 var ap = global.get_node("Archipelago")
176 var cmd = text.trim_suffix("\n")
177 entry.text = ""
178 if OS.is_debug_build():
179 if cmd.begins_with("/tp_map "):
180 var new_map = cmd.substr(8)
181 global.map = new_map
182 global.sets_entry_point = false
183 switcher.switch_map("res://objects/scenes/%s.tscn" % new_map)
184 return
185
186 ap.client.say(cmd)
187
188
189func update_locations(reset_locations = true):
190 var ap = global.get_node("Archipelago")
191 var gamedata = global.get_node("Gamedata")
192
193 locations_overlay.clear()
194 locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf"))
195 locations_overlay.push_font_size(24)
196 locations_overlay.push_color(Color(0.9, 0.9, 0.9, 1))
197 locations_overlay.push_outline_color(Color(0, 0, 0, 1))
198 locations_overlay.push_outline_size(2)
199
200 var locations = []
201 for location_id in ap.client._accessible_locations:
202 if not ap.client._checked_locations.has(location_id):
203 var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)")
204 (
205 locations
206 . append(
207 {
208 "name": location_name,
209 "type": kLocation,
210 "id": location_id,
211 }
212 )
213 )
214
215 for port_id in ap.client._accessible_worldports:
216 if not ap.client._checked_worldports.has(port_id):
217 var port_name = gamedata.get_worldport_display_name(port_id)
218 (
219 locations
220 . append(
221 {
222 "name": port_name,
223 "type": kWorldport,
224 "id": port_id,
225 }
226 )
227 )
228
229 locations.sort_custom(func(a, b): return a["name"] < b["name"])
230
231 if ap.client._goal_accessible:
232 var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
233 ap.victory_condition
234 ]]
235 (
236 locations
237 . push_front(
238 {
239 "name": location_name,
240 "type": kGoal,
241 }
242 )
243 )
244
245 var count = 0
246 for location in locations:
247 if count < 18:
248 locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT)
249 locations_overlay.append_text(location["name"])
250 locations_overlay.append_text(" ")
251 if location["type"] == kLocation:
252 locations_overlay.add_image(location_texture)
253 elif location["type"] == kWorldport:
254 locations_overlay.add_image(worldport_texture)
255 elif location["type"] == kGoal:
256 locations_overlay.add_image(goal_texture)
257 locations_overlay.pop()
258 count += 1
259
260 if count > 18:
261 locations_overlay.append_text("[p align=right][lb]...[rb][/p]")
262
263 if reset_locations:
264 reset_tracker_tab()
265
266 var root_ti = tracker_tree.create_item(null)
267
268 for location in locations:
269 var loc_row = root_ti.create_child()
270 loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON)
271 loc_row.set_selectable(0, false)
272 loc_row.set_text(1, location["name"])
273 loc_row.set_selectable(1, false)
274 loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM)
275 loc_row.set_text(2, "Show Path")
276 loc_row.set_custom_as_button(2, true)
277 loc_row.set_editable(2, true)
278 loc_row.set_selectable(2, false)
279 loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER)
280
281 if location["type"] == kLocation:
282 loc_row.set_icon(0, location_texture)
283 tracker_loc_tree_item_by_id[location["id"]] = loc_row
284 elif location["type"] == kWorldport:
285 loc_row.set_icon(0, worldport_texture)
286 tracker_port_tree_item_by_id[location["id"]] = loc_row
287 elif location["type"] == kGoal:
288 loc_row.set_icon(0, goal_texture)
289 tracker_goal_tree_item = loc_row
290
291 tracker_object_by_index[loc_row.get_index()] = location
292 else:
293 for loc_row in tracker_tree.get_root().get_children():
294 loc_row.visible = false
295
296 for location_id in tracker_loc_tree_item_by_id.keys():
297 if (
298 ap.client._accessible_locations.has(location_id)
299 and not ap.client._checked_locations.has(location_id)
300 ):
301 tracker_loc_tree_item_by_id[location_id].visible = true
302
303 for port_id in tracker_port_tree_item_by_id.keys():
304 if (
305 ap.client._accessible_worldports.has(port_id)
306 and not ap.client._checked_worldports.has(port_id)
307 ):
308 tracker_port_tree_item_by_id[port_id].visible = true
309
310 if tracker_goal_tree_item != null and ap.client._goal_accessible:
311 tracker_goal_tree_item.visible = true
312
313
314func update_locations_visibility():
315 var ap = global.get_node("Archipelago")
316 locations_overlay.visible = ap.show_locations
317
318
319func _on_tracker_button_clicked():
320 var edited_item = tracker_tree.get_edited()
321 var edited_index = edited_item.get_index()
322
323 if tracker_object_by_index.has(edited_index):
324 var tracker_object = tracker_object_by_index[edited_index]
325 var ap = global.get_node("Archipelago")
326 var type_str = ""
327 if tracker_object["type"] == kLocation:
328 type_str = "location"
329 elif tracker_object["type"] == kWorldport:
330 type_str = "worldport"
331 elif tracker_object["type"] == kGoal:
332 type_str = "goal"
333 ap.client.getLogicalPath(type_str, tracker_object.get("id", null))
334
335
336func display_logical_path(object_type, object_id, paths):
337 var ap = global.get_node("Archipelago")
338 var gamedata = global.get_node("Gamedata")
339
340 var location_name = "(Unknown)"
341 if object_type == "location" and object_id != null:
342 location_name = gamedata.location_name_by_id.get(object_id, "(Unknown)")
343 elif object_type == "worldport" and object_id != null:
344 location_name = gamedata.get_worldport_display_name(object_id)
345 elif object_type == "goal":
346 location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
347 ap.victory_condition
348 ]]
349
350 label.append_text("[p]Path to %s:[/p]" % location_name)
351 label.append_text("[ol]" + "\n".join(paths) + "[/ol]")
352
353 panel.visible = true
354
355
356func setup_worldports():
357 tabs.set_tab_hidden(2, false)
358
359 var root_ti = worldports_tree.create_item(null)
360
361 var ports_by_map_id = {}
362 var display_names_by_map_id = {}
363 var display_names_by_port_id = {}
364
365 var ap = global.get_node("Archipelago")
366 var gamedata = global.get_node("Gamedata")
367 for fpid in ap.port_pairings:
368 var port = gamedata.objects.get_ports()[fpid]
369 var room = gamedata.objects.get_rooms()[port.get_room_id()]
370
371 if not ports_by_map_id.has(room.get_map_id()):
372 ports_by_map_id[room.get_map_id()] = []
373
374 var map = gamedata.objects.get_maps()[room.get_map_id()]
375 display_names_by_map_id[map.get_id()] = map.get_display_name()
376
377 ports_by_map_id[room.get_map_id()].append(fpid)
378 display_names_by_port_id[fpid] = port.get_display_name()
379
380 var sorted_map_ids = ports_by_map_id.keys().duplicate()
381 sorted_map_ids.sort_custom(
382 func(a, b): return display_names_by_map_id[a] < display_names_by_map_id[b]
383 )
384
385 for map_id in sorted_map_ids:
386 var map_ti = root_ti.create_child()
387 map_ti.set_text(0, display_names_by_map_id[map_id])
388 map_ti.visible = false
389 map_ti.collapsed = true
390 port_tree_item_by_map[map_id] = map_ti
391 port_tree_item_by_map_port[map_id] = {}
392
393 var port_ids = ports_by_map_id[map_id]
394 port_ids.sort_custom(
395 func(a, b): return display_names_by_port_id[a] < display_names_by_port_id[b]
396 )
397
398 for port_id in port_ids:
399 var port_ti = map_ti.create_child()
400 port_ti.set_text(0, display_names_by_port_id[port_id])
401 port_ti.set_text(1, gamedata.get_worldport_display_name(ap.port_pairings[port_id]))
402 port_ti.visible = false
403 port_tree_item_by_map_port[map_id][port_id] = port_ti
404
405 update_worldports()
406
407
408func update_worldports():
409 var ap = global.get_node("Archipelago")
410
411 for map_id in port_tree_item_by_map_port.keys():
412 var map_visible = false
413
414 for port_id in port_tree_item_by_map_port[map_id].keys():
415 var ti = port_tree_item_by_map_port[map_id][port_id]
416 ti.visible = ap.client._checked_worldports.has(port_id)
417
418 if ti.visible:
419 map_visible = true
420
421 port_tree_item_by_map[map_id].visible = map_visible
422
423
424func reset():
425 locations_overlay.clear()
426 tabs.set_tab_hidden(2, true)
427 port_tree_item_by_map.clear()
428 port_tree_item_by_map_port.clear()
429 worldports_tree.clear()
430 reset_tracker_tab()
431
432
433func reset_tracker_tab():
434 tracker_loc_tree_item_by_id.clear()
435 tracker_port_tree_item_by_id.clear()
436 tracker_goal_tree_item = null
437 tracker_object_by_index.clear()
438 tracker_tree.clear()
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()