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.gd265
-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.gd286
-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.gd603
-rw-r--r--apworld/client/messages.gd74
-rw-r--r--apworld/client/minimap.gd175
-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.gd346
-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.gd153
-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.gd310
-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, 3840 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..62d7fd8 --- /dev/null +++ b/apworld/client/client.gd
@@ -0,0 +1,265 @@
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(message["checked_locations"]))
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"]),
162 loc["item"],
163 loc["player"],
164 int(loc["flags"]),
165 int(loc["for_self"])
166 )
167
168 elif cmd == "AccessibleLocations":
169 _accessible_locations.clear()
170 _accessible_worldports.clear()
171
172 for loc in message["locations"]:
173 _accessible_locations.append(int(loc))
174
175 if "worldports" in message:
176 for port_id in message["worldports"]:
177 _accessible_worldports.append(int(port_id))
178
179 _goal_accessible = bool(message.get("goal", false))
180
181 accessible_locations_updated.emit()
182
183 elif cmd == "UpdateKeyboard":
184 var updates = {}
185 for k in message["updates"]:
186 updates[k] = int(message["updates"][k])
187
188 keyboard_update_received.emit(updates)
189
190
191func connectToServer(server, un, pw):
192 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
193
194 ap_server = server
195 ap_user = un
196 ap_pass = pw
197
198 _should_process = true
199
200 connect_status.emit("Connecting...")
201
202
203func sendMessage(msg):
204 var payload = JSON.stringify(msg)
205 _server.send(0, payload)
206
207
208func connectToRoom():
209 connect_status.emit("Authenticating...")
210
211 sendMessage(
212 [
213 {
214 "cmd": "Connect",
215 "password": ap_pass,
216 "game": "Lingo 2",
217 "name": ap_user,
218 }
219 ]
220 )
221
222
223func requestSync():
224 sendMessage([{"cmd": "Sync"}])
225
226
227func sendLocation(loc_id):
228 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
229
230
231func sendLocations(loc_ids):
232 sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}])
233
234
235func say(textdata):
236 sendMessage([{"cmd": "Say", "text": textdata}])
237
238
239func completedGoal():
240 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
241
242
243func scoutLocations(loc_ids):
244 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
245
246
247func updateKeyboard(updates):
248 sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}])
249
250
251func checkWorldport(port_id):
252 if not _checked_worldports.has(port_id):
253 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}])
254
255
256func sendQuit():
257 sendMessage([{"cmd": "Quit"}])
258
259
260func hasItem(item_id):
261 return _received_items.has(item_id)
262
263
264func getItemAmount(item_id):
265 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..334d42a --- /dev/null +++ b/apworld/client/gamedata.gd
@@ -0,0 +1,286 @@
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 location_name_by_id[door.get_ap_id()] = _get_door_location_name(door)
76
77 for painting in objects.get_paintings():
78 var room = objects.get_rooms()[painting.get_room_id()]
79 var map = objects.get_maps()[room.get_map_id()]
80
81 if not map.get_name() in painting_id_by_map_node_path:
82 painting_id_by_map_node_path[map.get_name()] = {}
83
84 var _map_data = painting_id_by_map_node_path[map.get_name()]
85
86 for port in objects.get_ports():
87 var room = objects.get_rooms()[port.get_room_id()]
88 var map = objects.get_maps()[room.get_map_id()]
89
90 if not map.get_name() in port_id_by_map_node_path:
91 port_id_by_map_node_path[map.get_name()] = {}
92
93 var map_data = port_id_by_map_node_path[map.get_name()]
94 map_data[port.get_path()] = port.get_id()
95
96 for progressive in objects.get_progressives():
97 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
98
99 for letter in objects.get_letters():
100 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
101 location_name_by_id[letter.get_ap_id()] = _get_letter_location_name(letter)
102
103 for mastery in objects.get_masteries():
104 location_name_by_id[mastery.get_ap_id()] = _get_mastery_location_name(mastery)
105
106 for ending in objects.get_endings():
107 var location_name = _get_ending_location_name(ending)
108 location_name_by_id[ending.get_ap_id()] = location_name
109 ending_display_name_by_name[ending.get_name()] = location_name
110
111 for keyholder in objects.get_keyholders():
112 if keyholder.has_key():
113 location_name_by_id[keyholder.get_ap_id()] = _get_keyholder_location_name(keyholder)
114
115 for panel in objects.get_panels():
116 var room = objects.get_rooms()[panel.get_room_id()]
117 var map = objects.get_maps()[room.get_map_id()]
118
119 if not map.get_name() in panel_id_by_map_node_path:
120 panel_id_by_map_node_path[map.get_name()] = {}
121
122 var map_data = panel_id_by_map_node_path[map.get_name()]
123 map_data[panel.get_path()] = panel.get_id()
124
125 for symbol_name in kSYMBOL_ITEMS.values():
126 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
127
128 for special_name in objects.get_special_ids().keys():
129 if special_name.begins_with("Anti "):
130 anti_trap_ids[objects.get_special_ids()[special_name]] = (
131 special_name.substr(5).to_lower()
132 )
133
134
135func get_door_for_map_node_path(map_name, node_path):
136 if not door_id_by_map_node_path.has(map_name):
137 return null
138
139 var map_data = door_id_by_map_node_path[map_name]
140 return map_data.get(node_path, null)
141
142
143func get_panel_for_map_node_path(map_name, node_path):
144 if not panel_id_by_map_node_path.has(map_name):
145 return null
146
147 var map_data = panel_id_by_map_node_path[map_name]
148 return map_data.get(node_path, null)
149
150
151func get_port_for_map_node_path(map_name, node_path):
152 if not port_id_by_map_node_path.has(map_name):
153 return null
154
155 var map_data = port_id_by_map_node_path[map_name]
156 return map_data.get(node_path, null)
157
158
159func get_door_ap_id(door_id):
160 var door = objects.get_doors()[door_id]
161 if door.has_ap_id():
162 return door.get_ap_id()
163 else:
164 return null
165
166
167func get_door_map_name(door_id):
168 var door = objects.get_doors()[door_id]
169 var room = objects.get_rooms()[door.get_room_id()]
170 var map = objects.get_maps()[room.get_map_id()]
171 return map.get_name()
172
173
174func get_door_receivers(door_id):
175 var door = objects.get_doors()[door_id]
176 return door.get_receivers()
177
178
179func get_worldport_display_name(port_id):
180 var port = objects.get_ports()[port_id]
181 return "%s - %s" % [_get_room_object_map_name(port), port.get_display_name()]
182
183
184func _get_map_object_map_name(obj):
185 return objects.get_maps()[obj.get_map_id()].get_display_name()
186
187
188func _get_room_object_map_name(obj):
189 return _get_map_object_map_name(objects.get_rooms()[obj.get_room_id()])
190
191
192func _get_room_object_location_prefix(obj):
193 var room = objects.get_rooms()[obj.get_room_id()]
194 var game_map = objects.get_maps()[room.get_map_id()]
195
196 if room.has_panel_display_name():
197 return "%s (%s)" % [game_map.get_display_name(), room.get_panel_display_name()]
198 else:
199 return game_map.get_display_name()
200
201
202func _get_door_location_name(door):
203 var map_part = _get_room_object_location_prefix(door)
204
205 if door.has_location_name():
206 return "%s - %s" % [map_part, door.get_location_name()]
207
208 var generated_location_name = _get_generated_door_location_name(door)
209 if generated_location_name != null:
210 return generated_location_name
211
212 return "%s - %s" % [map_part, door.get_name()]
213
214
215func _get_generated_door_location_name(door):
216 if door.get_type() != SCRIPT_proto.DoorType.STANDARD:
217 return null
218
219 if door.get_keyholders().size() > 0 or door.get_endings().size() > 0 or door.has_complete_at():
220 return null
221
222 if door.get_panels().size() > 4:
223 return null
224
225 var map_areas = []
226 for panel_id in door.get_panels():
227 var panel = objects.get_panels()[panel_id.get_panel()]
228 var panel_room = objects.get_rooms()[panel.get_room_id()]
229 # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas.
230 if not map_areas.has(panel_room.get_panel_display_name()):
231 map_areas.append(panel_room.get_panel_display_name())
232
233 if map_areas.size() > 1:
234 return null
235
236 var game_map = objects.get_maps()[door.get_map_id()]
237 var map_area = map_areas[0]
238 var map_part
239 if map_area == "":
240 map_part = game_map.get_display_name()
241 else:
242 map_part = "%s (%s)" % [game_map.get_display_name(), map_area]
243
244 var panel_names = []
245 for panel_id in door.get_panels():
246 var panel_data = objects.get_panels()[panel_id.get_panel()]
247 var panel_name
248 if panel_data.has_display_name():
249 panel_name = panel_data.get_display_name()
250 else:
251 panel_name = panel_data.get_name()
252
253 var location_part
254 if panel_id.has_answer():
255 location_part = "%s/%s" % [panel_name, panel_id.get_answer().to_upper()]
256 else:
257 location_part = panel_name
258
259 panel_names.append(location_part)
260
261 panel_names.sort()
262
263 return map_part + " - " + ", ".join(panel_names)
264
265
266func _get_letter_location_name(letter):
267 var letter_level = 2 if letter.get_level2() else 1
268 var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level]
269 return "%s - %s" % [_get_room_object_map_name(letter), letter_name]
270
271
272func _get_mastery_location_name(mastery):
273 return "%s - Mastery" % _get_room_object_map_name(mastery)
274
275
276func _get_ending_location_name(ending):
277 return (
278 "%s - %s Ending" % [_get_room_object_map_name(ending), ending.get_name().to_pascal_case()]
279 )
280
281
282func _get_keyholder_location_name(keyholder):
283 return (
284 "%s - %s Keyholder"
285 % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()]
286 )
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..9212233 --- /dev/null +++ b/apworld/client/manager.gd
@@ -0,0 +1,603 @@
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
78signal could_not_connect
79signal connect_status
80signal ap_connected
81
82
83func _init():
84 # Read AP settings from file, if there are any
85 if FileAccess.file_exists("user://ap_settings"):
86 var file = FileAccess.open("user://ap_settings", FileAccess.READ)
87 var data = file.get_var(true)
88 file.close()
89
90 if typeof(data) != TYPE_ARRAY:
91 global._print("AP settings file is corrupted")
92 data = []
93
94 if data.size() > 0:
95 ap_server = data[0]
96
97 if data.size() > 1:
98 ap_user = data[1]
99
100 if data.size() > 2:
101 ap_pass = data[2]
102
103 if data.size() > 3:
104 connection_history = data[3]
105
106 if data.size() > 4:
107 show_compass = data[4]
108
109 if data.size() > 5:
110 show_locations = data[5]
111
112 if data.size() > 6:
113 show_minimap = data[6]
114
115
116func _ready():
117 client = SCRIPT_client.new()
118 client.SCRIPT_websocketserver = SCRIPT_websocketserver
119
120 client.item_received.connect(_process_item)
121 client.location_scout_received.connect(_process_location_scout)
122 client.text_message_received.connect(_process_text_message)
123 client.item_sent_notification.connect(_process_item_sent_notification)
124 client.hint_received.connect(_process_hint_received)
125 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
126 client.checked_locations_updated.connect(_on_checked_locations_updated)
127 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
128
129 client.could_not_connect.connect(_client_could_not_connect)
130 client.connect_status.connect(_client_connect_status)
131 client.client_connected.connect(_client_connected)
132
133 add_child(client)
134
135 keyboard = SCRIPT_keyboard.new()
136 add_child(keyboard)
137 client.keyboard_update_received.connect(keyboard.remote_keyboard_updated)
138
139
140func saveSettings():
141 # Save the AP settings to disk.
142 var path = "user://ap_settings"
143 var file = FileAccess.open(path, FileAccess.WRITE)
144
145 var data = [
146 ap_server,
147 ap_user,
148 ap_pass,
149 connection_history,
150 show_compass,
151 show_locations,
152 show_minimap,
153 ]
154 file.store_var(data, true)
155 file.close()
156
157
158func saveLocaldata():
159 # Save the MW/slot specific settings to disk.
160 var dir = DirAccess.open("user://")
161 var folder = "archipelago_data"
162 if not dir.dir_exists(folder):
163 dir.make_dir(folder)
164
165 var file = FileAccess.open(_localdata_file, FileAccess.WRITE)
166
167 var data = [
168 _last_new_item,
169 ]
170 file.store_var(data, true)
171 file.close()
172
173
174func connectToServer():
175 _last_new_item = -1
176 _batch_locations = false
177 _held_locations = []
178 _held_location_scouts = []
179 _location_scouts = {}
180 _letters_setup = false
181 _held_letters = {}
182 _already_connected = false
183
184 client.connectToServer(ap_server, ap_user, ap_pass)
185
186
187func getSaveFileName():
188 return "zzAP_%s_%d" % [client._seed, client._slot]
189
190
191func disconnect_from_ap():
192 _already_connected = false
193
194 var effects = global.get_node("Effects")
195 effects.set_connection_lost(false)
196
197 client.disconnect_from_ap()
198
199
200func get_item_id_for_door(door_id):
201 return _item_locks.get(door_id, null)
202
203
204func _process_item(item, amount):
205 var gamedata = global.get_node("Gamedata")
206
207 var item_id = int(item["id"])
208 var prog_id = null
209 if _inverse_item_locks.has(item_id):
210 for lock in _inverse_item_locks.get(item_id):
211 if lock[1] != amount:
212 continue
213
214 if gamedata.progressive_id_by_ap_id.has(item_id):
215 prog_id = lock[0]
216
217 if gamedata.get_door_map_name(lock[0]) != global.map:
218 continue
219
220 # TODO: fix doors opening from door groups
221 var receivers = gamedata.get_door_receivers(lock[0])
222 var scene = get_tree().get_root().get_node_or_null("scene")
223 if scene != null:
224 for receiver in receivers:
225 var rnode = scene.get_node_or_null(receiver)
226 if rnode != null:
227 rnode.handleTriggered()
228
229 var letter_id = gamedata.letter_id_by_ap_id.get(item_id, null)
230 if letter_id != null:
231 var letter = gamedata.objects.get_letters()[letter_id]
232 if not letter.has_level2() or not letter.get_level2():
233 _process_key_item(letter.get_key(), amount)
234
235 if gamedata.symbol_item_ids.has(item_id):
236 var player = get_tree().get_root().get_node_or_null("scene/player")
237 if player != null:
238 player.evaluate_solvability.emit()
239
240 # Show a message about the item if it's new.
241 if int(item["index"]) > _last_new_item:
242 _last_new_item = int(item["index"])
243 saveLocaldata()
244
245 var full_item_name = item["text"]
246 if prog_id != null:
247 var door = gamedata.objects.get_doors()[prog_id]
248 full_item_name = "%s (%s)" % [full_item_name, door.get_name()]
249
250 var message
251 if "sender" in item:
252 message = (
253 "Received %s from %s"
254 % [wrapInItemColorTags(full_item_name, item["flags"]), item["sender"]]
255 )
256 else:
257 message = "Found %s" % wrapInItemColorTags(full_item_name, item["flags"])
258
259 if gamedata.anti_trap_ids.has(item):
260 keyboard.block_letter(gamedata.anti_trap_ids[item])
261
262 global._print(message)
263
264 global.get_node("Messages").showMessage(message)
265
266
267func _process_item_sent_notification(message):
268 var sentMsg = (
269 "Sent %s to %s"
270 % [
271 wrapInItemColorTags(message["item_name"], message["item_flags"]),
272 message["receiver_name"]
273 ]
274 )
275 #if _hinted_locations.has(message["item"]["location"]):
276 # sentMsg += " ([color=#fafad2]Hinted![/color])"
277 global.get_node("Messages").showMessage(sentMsg)
278
279
280func _process_hint_received(message):
281 var is_for = ""
282 if message["self"] == 0:
283 is_for = " for %s" % message["receiver_name"]
284
285 global.get_node("Messages").showMessage(
286 (
287 "Hint: %s%s is on %s"
288 % [
289 wrapInItemColorTags(message["item_name"], message["item_flags"]),
290 is_for,
291 message["location_name"]
292 ]
293 )
294 )
295
296
297func _process_text_message(message):
298 var parts = []
299 for message_part in message:
300 if message_part["type"] == "text":
301 parts.append(message_part["text"])
302 elif message_part["type"] == "player":
303 if message_part["self"] == 1:
304 parts.append("[color=#ee00ee]%s[/color]" % message_part["text"])
305 else:
306 parts.append("[color=#fafad2]%s[/color]" % message_part["text"])
307 elif message_part["type"] == "item":
308 parts.append(wrapInItemColorTags(message_part["text"], int(message_part["flags"])))
309 elif message_part["type"] == "location":
310 parts.append("[color=#00ff7f]%s[/color]" % message_part["text"])
311
312 var textclient_node = global.get_node("Textclient")
313 if textclient_node != null:
314 textclient_node.parse_printjson("".join(parts))
315
316
317func _process_location_scout(location_id, item_name, player_name, flags, for_self):
318 _location_scouts[location_id] = {
319 "item": item_name, "player": player_name, "flags": flags, "for_self": for_self
320 }
321
322 if for_self and flags & 4 != 0:
323 # This is a trap for us, so let's not display it.
324 return
325
326 var gamedata = global.get_node("Gamedata")
327 var map_id = gamedata.map_id_by_name.get(global.map)
328
329 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
330 if letter_id != null:
331 var letter = gamedata.objects.get_letters()[letter_id]
332 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
333 if room.get_map_id() == map_id:
334 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
335 letter.get_path()
336 )
337 if collectable != null:
338 collectable.setScoutedText(item_name)
339
340
341func _on_accessible_locations_updated():
342 var textclient_node = global.get_node("Textclient")
343 if textclient_node != null:
344 textclient_node.update_locations()
345
346
347func _on_checked_locations_updated():
348 var textclient_node = global.get_node("Textclient")
349 if textclient_node != null:
350 textclient_node.update_locations()
351
352
353func _on_checked_worldports_updated():
354 var textclient_node = global.get_node("Textclient")
355 if textclient_node != null:
356 textclient_node.update_locations()
357 textclient_node.update_worldports()
358
359
360func _client_could_not_connect(message):
361 could_not_connect.emit(message)
362
363 if global.loaded:
364 var effects = global.get_node("Effects")
365 effects.set_connection_lost(true)
366
367 var messages = global.get_node("Messages")
368 messages.showMessage("Connection to multiworld lost.")
369
370
371func _client_connect_status(message):
372 connect_status.emit(message)
373
374
375func _client_connected(slot_data):
376 var effects = global.get_node("Effects")
377 effects.set_connection_lost(false)
378
379 if _already_connected:
380 var messages = global.get_node("Messages")
381 messages.showMessage("Reconnected to multiworld!")
382 return
383
384 _already_connected = true
385
386 var gamedata = global.get_node("Gamedata")
387
388 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
389 _last_new_item = -1
390
391 if FileAccess.file_exists(_localdata_file):
392 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
393 var localdata = []
394 if ap_file != null:
395 localdata = ap_file.get_var(true)
396 ap_file.close()
397
398 if typeof(localdata) != TYPE_ARRAY:
399 print("AP localdata file is corrupted")
400 localdata = []
401
402 if localdata.size() > 0:
403 _last_new_item = localdata[0]
404
405 # Read slot data.
406 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
407 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
408 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
409 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
410 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
411 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
412 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
413 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
414 shuffle_worldports = bool(slot_data.get("shuffle_worldports", false))
415 strict_cyan_ending = bool(slot_data.get("strict_cyan_ending", false))
416 strict_purple_ending = bool(slot_data.get("strict_purple_ending", false))
417 victory_condition = int(slot_data.get("victory_condition", 0))
418
419 if slot_data.has("version"):
420 var version_msg = slot_data["version"]
421 apworld_version = [int(version_msg[0]), int(version_msg[1]), 0]
422 if version_msg.size() > 2:
423 apworld_version[2] = int(version_msg[2])
424
425 port_pairings.clear()
426 if slot_data.has("port_pairings"):
427 var raw_pp = slot_data.get("port_pairings")
428
429 for p1 in raw_pp.keys():
430 port_pairings[int(p1)] = int(raw_pp[p1])
431
432 # Set up item locks.
433 _item_locks = {}
434
435 if shuffle_doors:
436 for door in gamedata.objects.get_doors():
437 if (
438 door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD
439 or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
440 ):
441 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
442
443 for progressive in gamedata.objects.get_progressives():
444 for i in range(0, progressive.get_doors().size()):
445 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
446 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
447
448 for door_group in gamedata.objects.get_door_groups():
449 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR:
450 if shuffle_worldports:
451 continue
452 elif door_group.get_type() != gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP:
453 continue
454
455 for door in door_group.get_doors():
456 _item_locks[door] = [door_group.get_ap_id(), 1]
457
458 if shuffle_control_center_colors:
459 for door in gamedata.objects.get_doors():
460 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
461 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
462
463 for door_group in gamedata.objects.get_door_groups():
464 if (
465 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR
466 and not shuffle_worldports
467 ):
468 for door in door_group.get_doors():
469 _item_locks[door] = [door_group.get_ap_id(), 1]
470
471 if shuffle_gallery_paintings:
472 for door in gamedata.objects.get_doors():
473 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
474 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
475
476 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
477 for door_group in gamedata.objects.get_door_groups():
478 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
479 for door in door_group.get_doors():
480 if not _item_locks.has(door):
481 _item_locks[door] = [door_group.get_ap_id(), 1]
482
483 # Create a reverse item locks map for processing items.
484 _inverse_item_locks = {}
485
486 for door_id in _item_locks.keys():
487 var lock = _item_locks.get(door_id)
488
489 if not _inverse_item_locks.has(lock[0]):
490 _inverse_item_locks[lock[0]] = []
491
492 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
493
494 if shuffle_worldports:
495 var textclient = global.get_node("Textclient")
496 textclient.setup_worldports()
497
498 ap_connected.emit()
499
500
501func start_batching_locations():
502 _batch_locations = true
503
504
505func send_location(loc_id):
506 if _batch_locations:
507 _held_locations.append(loc_id)
508 else:
509 client.sendLocation(loc_id)
510
511
512func scout_location(loc_id):
513 if _location_scouts.has(loc_id):
514 return _location_scouts.get(loc_id)
515
516 if _batch_locations:
517 _held_location_scouts.append(loc_id)
518 else:
519 client.scoutLocation(loc_id)
520
521 return null
522
523
524func stop_batching_locations():
525 _batch_locations = false
526
527 if not _held_locations.is_empty():
528 client.sendLocations(_held_locations)
529 _held_locations.clear()
530
531 if not _held_location_scouts.is_empty():
532 client.scoutLocations(_held_location_scouts)
533 _held_location_scouts.clear()
534
535
536func colorForItemType(flags):
537 var int_flags = int(flags)
538 if int_flags & 1: # progression
539 if int_flags & 2: # proguseful
540 return "#f0d200"
541 else:
542 return "#bc51e0"
543 elif int_flags & 2: # useful
544 return "#2b67ff"
545 elif int_flags & 4: # trap
546 return "#d63a22"
547 else: # filler
548 return "#14de9e"
549
550
551func wrapInItemColorTags(text, flags):
552 var int_flags = int(flags)
553 if int_flags & 1 and int_flags & 2: # proguseful
554 return "[rainbow]%s[/rainbow]" % text
555 else:
556 return "[color=%s]%s[/color]" % [colorForItemType(flags), text]
557
558
559func get_letter_behavior(key, level2):
560 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
561 return kLETTER_BEHAVIOR_UNLOCKED
562
563 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
564 if level2:
565 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
566 return kLETTER_BEHAVIOR_VANILLA
567 else:
568 return kLETTER_BEHAVIOR_ITEM
569 else:
570 return kLETTER_BEHAVIOR_UNLOCKED
571
572 if not level2 and ["h", "i", "n", "t"].has(key):
573 # This differs from the equivalent function in the apworld. Logically it is
574 # the same as UNLOCKED since they are in the starting room, but VANILLA
575 # means the player still has to actually pick up the letters.
576 return kLETTER_BEHAVIOR_VANILLA
577
578 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
579 return kLETTER_BEHAVIOR_ITEM
580
581 return kLETTER_BEHAVIOR_VANILLA
582
583
584func setup_keys():
585 keyboard.load_seed()
586
587 _letters_setup = true
588
589 for k in _held_letters.keys():
590 _process_key_item(k, _held_letters[k])
591
592 _held_letters.clear()
593
594
595func _process_key_item(key, level):
596 if not _letters_setup:
597 _held_letters[key] = max(_held_letters.get(key, 0), level)
598 return
599
600 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
601 level += 1
602
603 keyboard.collect_remote_letter(key, level)
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..5640716 --- /dev/null +++ b/apworld/client/minimap.gd
@@ -0,0 +1,175 @@
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 heights = {}
130
131 var rendered = Image.create_empty(cell_width, cell_height, false, Image.FORMAT_RGBA8)
132 rendered.fill(Color.TRANSPARENT)
133
134 var meshes_node = get_tree().get_root().get_node("scene/Meshes")
135 if meshes_node != null:
136 _renderMeshNode(gridmap, meshes_node, rendered)
137
138 for pos in gridmap.get_used_cells():
139 var in_plane = Vector2i(pos.x, pos.z)
140
141 if in_plane in heights and heights[in_plane] > pos.y:
142 continue
143
144 heights[in_plane] = pos.y
145
146 var cell_item = gridmap.get_cell_item(pos)
147 var mesh = gridmap.mesh_library.get_item_mesh(cell_item)
148 var material = mesh.surface_get_material(0)
149 var color = material.albedo_color
150
151 rendered.set_pixel(pos.x - cell_left, pos.z - cell_top, color)
152
153 return rendered
154
155
156func _renderMeshNode(gridmap, mesh, rendered):
157 if mesh is MeshInstance3D:
158 var local_tl = gridmap.map_to_local(Vector3i(cell_left, 0, cell_top))
159 var global_tl = gridmap.to_global(local_tl)
160 var mesh_material = mesh.get_surface_override_material(0)
161 if mesh_material != null:
162 var mesh_color = mesh_material.albedo_color
163
164 for y in range(
165 max(mesh.position.z - mesh.scale.z / 2 - global_tl.z, 0),
166 min(mesh.position.z + mesh.scale.z / 2 - global_tl.z, cell_height)
167 ):
168 for x in range(
169 max(mesh.position.x - mesh.scale.x / 2 - global_tl.x, 0),
170 min(mesh.position.x + mesh.scale.x / 2 - global_tl.x, cell_width)
171 ):
172 rendered.set_pixel(x, y, mesh_color)
173
174 for child in mesh.get_children():
175 _renderMeshNode(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..5417a48 --- /dev/null +++ b/apworld/client/player.gd
@@ -0,0 +1,346 @@
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 ):
35 continue
36
37 var locationListener = ap.SCRIPT_locationListener.new()
38 locationListener.location_id = door.get_ap_id()
39 locationListener.name = "locationListener_%d" % door.get_ap_id()
40
41 for panel_ref in door.get_panels():
42 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
43 var panel_path = panel_data.get_path()
44
45 if panel_ref.has_answer():
46 for proxy in panel_data.get_proxies():
47 if proxy.get_answer() == panel_ref.get_answer():
48 panel_path = proxy.get_path()
49 break
50
51 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
52
53 for keyholder_ref in door.get_keyholders():
54 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
55
56 var khl = khl_script.new()
57 khl.name = (
58 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
59 )
60 khl.answer = keyholder_ref.get_key()
61 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
62 get_parent().add_child.call_deferred(khl)
63
64 locationListener.senders.append(NodePath("../" + khl.name))
65
66 for sender in door.get_senders():
67 locationListener.senders.append(NodePath("/root/scene/" + sender))
68
69 if door.has_complete_at():
70 locationListener.complete_at = door.get_complete_at()
71
72 get_parent().add_child.call_deferred(locationListener)
73
74 # Set up letter locations.
75 for letter in gamedata.objects.get_letters():
76 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
77 if room.get_map_id() != map_id:
78 continue
79
80 var locationListener = ap.SCRIPT_locationListener.new()
81 locationListener.location_id = letter.get_ap_id()
82 locationListener.name = "locationListener_%d" % letter.get_ap_id()
83 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
84
85 get_parent().add_child.call_deferred(locationListener)
86
87 if (
88 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
89 != ap.kLETTER_BEHAVIOR_VANILLA
90 ):
91 var scout = ap.scout_location(letter.get_ap_id())
92 if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0):
93 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
94 letter.get_path()
95 )
96 if collectable != null:
97 collectable.setScoutedText.call_deferred(scout["item"])
98
99 # Set up mastery locations.
100 for mastery in gamedata.objects.get_masteries():
101 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
102 if room.get_map_id() != map_id:
103 continue
104
105 var locationListener = ap.SCRIPT_locationListener.new()
106 locationListener.location_id = mastery.get_ap_id()
107 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
108 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
109
110 get_parent().add_child.call_deferred(locationListener)
111
112 # Set up ending locations.
113 for ending in gamedata.objects.get_endings():
114 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
115 if room.get_map_id() != map_id:
116 continue
117
118 var locationListener = ap.SCRIPT_locationListener.new()
119 locationListener.location_id = ending.get_ap_id()
120 locationListener.name = "locationListener_%d" % ending.get_ap_id()
121 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
122
123 get_parent().add_child.call_deferred(locationListener)
124
125 if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
126 var victoryListener = ap.SCRIPT_victoryListener.new()
127 victoryListener.name = "victoryListener"
128 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
129
130 get_parent().add_child.call_deferred(victoryListener)
131
132 # Set up keyholder locations, in keyholder sanity.
133 if ap.keyholder_sanity:
134 for keyholder in gamedata.objects.get_keyholders():
135 if not keyholder.has_key():
136 continue
137
138 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
139 if room.get_map_id() != map_id:
140 continue
141
142 var locationListener = ap.SCRIPT_locationListener.new()
143 locationListener.location_id = keyholder.get_ap_id()
144 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
145
146 var khl = khl_script.new()
147 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
148 khl.answer = keyholder.get_key()
149 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
150 get_parent().add_child.call_deferred(khl)
151
152 locationListener.senders.append(NodePath("../" + khl.name))
153
154 get_parent().add_child.call_deferred(locationListener)
155
156 # Block off roof access in Daedalus.
157 if global.map == "daedalus" and not ap.daedalus_roof_access:
158 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
159 _set_up_invis_wall(51.5, 11, -17, 16, 10, 1)
160 _set_up_invis_wall(46, 10, -9.5, 1, 10, 10)
161 _set_up_invis_wall(67.5, 11, 17, 16, 10, 1)
162 _set_up_invis_wall(50.5, 11, 14, 10, 10, 1)
163 _set_up_invis_wall(39, 10, 18.5, 1, 10, 22)
164 _set_up_invis_wall(20, 15, 18.5, 1, 10, 16)
165 _set_up_invis_wall(11.5, 15, 3, 32, 10, 1)
166 _set_up_invis_wall(11.5, 16, -20, 14, 20, 1)
167 _set_up_invis_wall(14, 16, -26.5, 1, 20, 4)
168 _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25)
169 _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1)
170 _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1)
171 _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11)
172 _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11)
173
174 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
175 var warp_exit = warp_exit_prefab.instantiate()
176 warp_exit.name = "roof_access_blocker_warp_exit"
177 warp_exit.position = Vector3(58, 10, 0)
178 warp_exit.rotation_degrees.y = 90
179 get_parent().add_child.call_deferred(warp_exit)
180
181 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
182 var warp_enter = warp_enter_prefab.instantiate()
183 warp_enter.target = warp_exit
184 warp_enter.position = Vector3(76.5, 30, 1)
185 warp_enter.scale = Vector3(4, 1.5, 1)
186 warp_enter.rotation_degrees.y = 90
187 get_parent().add_child.call_deferred(warp_enter)
188
189 if global.map == "the_entry":
190 # Remove door behind X1.
191 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
192 door_node.handleTriggered()
193
194 # Display win condition.
195 var sign_prefab = preload("res://objects/nodes/sign.tscn")
196 var sign1 = sign_prefab.instantiate()
197 sign1.position = Vector3(-7, 5, -15.01)
198 sign1.text = "victory"
199 get_parent().add_child.call_deferred(sign1)
200
201 var sign2 = sign_prefab.instantiate()
202 sign2.position = Vector3(-7, 4, -15.01)
203 sign2.text = "%s ending" % ap.kEndingNameByVictoryValue.get(ap.victory_condition, "?")
204
205 var sign2_color = ap.kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
206 if sign2_color == "white":
207 sign2_color = "silver"
208
209 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
210 get_parent().add_child.call_deferred(sign2)
211
212 # Add the strict purple ending validation.
213 if global.map == "the_sun_temple" and ap.strict_purple_ending:
214 var panel_prefab = preload("res://objects/nodes/panel.tscn")
215 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
216 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
217
218 var previous_panel = null
219 var next_y = -100
220 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
221 for word in words:
222 var panel = panel_prefab.instantiate()
223 panel.position = Vector3(0, next_y, 0)
224 next_y -= 10
225 panel.clue = word
226 panel.symbol = ""
227 panel.answer = word
228 panel.name = "EndCheck_%s" % word
229
230 var tpl = tpl_prefab.instantiate()
231 tpl.teleport_point = Vector3(0, 1, 0)
232 tpl.teleport_rotate = Vector3(-45, 180, 0)
233 tpl.target_path = panel
234 tpl.name = "Teleport"
235
236 if previous_panel == null:
237 tpl.senders.append(NodePath("/root/scene/Panels/End/panel_24"))
238 else:
239 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
240
241 var reversing = reverse_prefab.instantiate()
242 reversing.senders.append(NodePath(".."))
243 reversing.name = "Reversing"
244 tpl.senders.append(NodePath("../Reversing"))
245
246 panel.add_child.call_deferred(tpl)
247 panel.add_child.call_deferred(reversing)
248 get_parent().get_node("Panels").add_child.call_deferred(panel)
249
250 previous_panel = panel
251
252 # Duplicate the doors that usually wait on EQUINOX. We can't set the senders
253 # here for some reason so we actually set them in the door ready function.
254 var endplat = get_node("/root/scene/Components/Doors/EndPlatform")
255 var endplat2 = endplat.duplicate()
256 endplat2.name = "spe_EndPlatform"
257 endplat.get_parent().add_child.call_deferred(endplat2)
258 endplat.queue_free()
259
260 var entry2 = get_node("/root/scene/Components/Doors/entry_2")
261 var entry22 = entry2.duplicate()
262 entry22.name = "spe_entry_2"
263 entry2.get_parent().add_child.call_deferred(entry22)
264 entry2.queue_free()
265
266 # Add the strict cyan ending validation.
267 if global.map == "the_parthenon" and ap.strict_cyan_ending:
268 var panel_prefab = preload("res://objects/nodes/panel.tscn")
269 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
270 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
271
272 var previous_panel = null
273 var next_y = -100
274 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
275 for word in words:
276 var panel = panel_prefab.instantiate()
277 panel.position = Vector3(0, next_y, 0)
278 next_y -= 10
279 panel.clue = word
280 panel.symbol = "."
281 panel.answer = "%s%s" % [word, word]
282 panel.name = "EndCheck_%s" % word
283
284 var tpl = tpl_prefab.instantiate()
285 tpl.teleport_point = Vector3(0, 1, -11)
286 tpl.teleport_rotate = Vector3(-45, 0, 0)
287 tpl.target_path = panel
288 tpl.name = "Teleport"
289
290 if previous_panel == null:
291 tpl.senderGroup.append(NodePath("/root/scene/Panels/Rulers"))
292 else:
293 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
294
295 var reversing = reverse_prefab.instantiate()
296 reversing.senders.append(NodePath(".."))
297 reversing.name = "Reversing"
298 tpl.senders.append(NodePath("../Reversing"))
299
300 panel.add_child.call_deferred(tpl)
301 panel.add_child.call_deferred(reversing)
302 get_parent().get_node("Panels").add_child.call_deferred(panel)
303
304 previous_panel = panel
305
306 # Duplicate the door that usually waits on the rulers. We can't set the
307 # senders here for some reason so we actually set them in the door ready
308 # function.
309 var entry1 = get_node("/root/scene/Components/Doors/entry_1")
310 var entry12 = entry1.duplicate()
311 entry12.name = "spe_entry_1"
312 entry1.get_parent().add_child.call_deferred(entry12)
313 entry1.queue_free()
314
315 var minimap = ap.SCRIPT_minimap.new()
316 minimap.name = "Minimap"
317 minimap.visible = ap.show_minimap
318 get_parent().add_child.call_deferred(minimap)
319
320 super._ready()
321
322 await get_tree().process_frame
323 await get_tree().process_frame
324
325 ap.stop_batching_locations()
326
327
328func _set_up_invis_wall(x, y, z, sx, sy, sz):
329 var prefab = preload("res://objects/nodes/block.tscn")
330 var newwall = prefab.instantiate()
331 newwall.position.x = x
332 newwall.position.y = y
333 newwall.position.z = z
334 newwall.scale.x = sz
335 newwall.scale.y = sy
336 newwall.scale.z = sx
337 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
338 newwall.visibility_range_end = 3
339 newwall.visibility_range_end_margin = 1
340 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
341 newwall.skeleton = ".."
342 get_parent().add_child.call_deferred(newwall)
343
344
345func _process(_dt):
346 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..b430b17 --- /dev/null +++ b/apworld/client/settings_screen.gd
@@ -0,0 +1,153 @@
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.horizontal_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.horizontal_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.horizontal_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 accept_dialog.offset_right = 83.0
130 accept_dialog.offset_bottom = 58.0
131 panel.add_child(accept_dialog)
132
133 var version_mismatch = ConfirmationDialog.new()
134 version_mismatch.name = "VersionMismatch"
135 version_mismatch.offset_right = 83.0
136 version_mismatch.offset_bottom = 58.0
137 panel.add_child(version_mismatch)
138
139 var connection_history = MenuButton.new()
140 connection_history.name = "connection_history"
141 connection_history.offset_left = 1239.0
142 connection_history.offset_top = 276.0
143 connection_history.offset_right = 1829.0
144 connection_history.offset_bottom = 372.0
145 connection_history.text = "connection history"
146 connection_history.flat = false
147 panel.add_child(connection_history)
148
149 var runtime = global.get_node("Runtime")
150 var main_script = runtime.load_script("main.gd")
151 var main_node = main_script.new()
152 main_node.name = "Main"
153 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..530eddb --- /dev/null +++ b/apworld/client/textclient.gd
@@ -0,0 +1,310 @@
1extends CanvasLayer
2
3var tabs
4var panel
5var label
6var entry
7var tracker_label
8var is_open = false
9
10var locations_overlay
11var location_texture
12var worldport_texture
13var goal_texture
14
15var worldports_tab
16var worldports_tree
17var port_tree_item_by_map = {}
18var port_tree_item_by_map_port = {}
19
20
21func _ready():
22 process_mode = ProcessMode.PROCESS_MODE_ALWAYS
23 layer = 2
24
25 locations_overlay = RichTextLabel.new()
26 locations_overlay.name = "LocationsOverlay"
27 locations_overlay.offset_top = 220
28 locations_overlay.offset_bottom = 720
29 locations_overlay.offset_left = 20
30 locations_overlay.anchor_right = 1.0
31 locations_overlay.offset_right = -10
32 locations_overlay.scroll_active = false
33 locations_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
34 locations_overlay.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
35 add_child(locations_overlay)
36 update_locations_visibility()
37
38 tabs = TabContainer.new()
39 tabs.name = "Tabs"
40 tabs.offset_left = 100
41 tabs.offset_right = 1820
42 tabs.offset_top = 100
43 tabs.offset_bottom = 980
44 tabs.visible = false
45 tabs.theme = preload("res://assets/themes/baseUI.tres")
46 tabs.add_theme_font_size_override("font_size", 36)
47 add_child(tabs)
48
49 panel = MarginContainer.new()
50 panel.name = "Text Client"
51 panel.add_theme_constant_override("margin_top", 60)
52 panel.add_theme_constant_override("margin_left", 60)
53 panel.add_theme_constant_override("margin_right", 60)
54 panel.add_theme_constant_override("margin_bottom", 60)
55 tabs.add_child(panel)
56
57 label = RichTextLabel.new()
58 label.set_name("Label")
59 label.scroll_following = true
60 label.selection_enabled = true
61 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
62 label.size_flags_vertical = Control.SIZE_EXPAND_FILL
63 label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
64 label.push_font_size(36)
65
66 var entry_style = StyleBoxFlat.new()
67 entry_style.bg_color = Color(0.9, 0.9, 0.9, 1)
68
69 entry = LineEdit.new()
70 entry.set_name("Entry")
71 entry.add_theme_font_override("font", preload("res://assets/fonts/Lingo2.ttf"))
72 entry.add_theme_font_size_override("font_size", 36)
73 entry.add_theme_color_override("font_color", Color(0, 0, 0, 1))
74 entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1))
75 entry.add_theme_stylebox_override("focus", entry_style)
76 entry.text_submitted.connect(text_entered)
77
78 var tc_arranger = VBoxContainer.new()
79 tc_arranger.add_child(label)
80 tc_arranger.add_child(entry)
81 tc_arranger.add_theme_constant_override("separation", 40)
82 panel.add_child(tc_arranger)
83
84 var tracker_margins = MarginContainer.new()
85 tracker_margins.name = "Locations"
86 tracker_margins.add_theme_constant_override("margin_top", 60)
87 tracker_margins.add_theme_constant_override("margin_left", 60)
88 tracker_margins.add_theme_constant_override("margin_right", 60)
89 tracker_margins.add_theme_constant_override("margin_bottom", 60)
90 tabs.add_child(tracker_margins)
91
92 tracker_label = RichTextLabel.new()
93 tracker_margins.add_child(tracker_label)
94
95 worldports_tab = MarginContainer.new()
96 worldports_tab.name = "Worldports"
97 worldports_tab.add_theme_constant_override("margin_top", 60)
98 worldports_tab.add_theme_constant_override("margin_left", 60)
99 worldports_tab.add_theme_constant_override("margin_right", 60)
100 worldports_tab.add_theme_constant_override("margin_bottom", 60)
101 tabs.add_child(worldports_tab)
102 tabs.set_tab_hidden(2, true)
103
104 worldports_tree = Tree.new()
105 worldports_tree.columns = 2
106 worldports_tree.hide_root = true
107 worldports_tree.theme = preload("res://assets/themes/baseUI.tres")
108 worldports_tree.add_theme_font_size_override("font_size", 24)
109 worldports_tab.add_child(worldports_tree)
110
111 var runtime = global.get_node("Runtime")
112 var location_image = Image.new()
113 location_image.load_png_from_buffer(runtime.read_path("assets/location.png"))
114 location_texture = ImageTexture.create_from_image(location_image)
115
116 var worldport_image = Image.new()
117 worldport_image.load_png_from_buffer(runtime.read_path("assets/worldport.png"))
118 worldport_texture = ImageTexture.create_from_image(worldport_image)
119
120 var goal_image = Image.new()
121 goal_image.load_png_from_buffer(runtime.read_path("assets/goal.png"))
122 goal_texture = ImageTexture.create_from_image(goal_image)
123
124
125func _input(event):
126 if global.loaded and event is InputEventKey and event.pressed:
127 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
128 if !get_tree().paused:
129 is_open = true
130 get_tree().paused = true
131 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
132 tabs.visible = true
133 entry.grab_focus()
134 get_viewport().set_input_as_handled()
135 else:
136 dismiss()
137 elif event.keycode == KEY_ESCAPE:
138 if is_open:
139 dismiss()
140 get_viewport().set_input_as_handled()
141
142
143func dismiss():
144 if is_open:
145 get_tree().paused = false
146 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
147 tabs.visible = false
148 is_open = false
149
150
151func parse_printjson(text):
152 label.append_text("[p]" + text + "[/p]")
153
154
155func text_entered(text):
156 var ap = global.get_node("Archipelago")
157 var cmd = text.trim_suffix("\n")
158 entry.text = ""
159 if OS.is_debug_build():
160 if cmd.begins_with("/tp_map "):
161 var new_map = cmd.substr(8)
162 global.map = new_map
163 global.sets_entry_point = false
164 switcher.switch_map("res://objects/scenes/%s.tscn" % new_map)
165 return
166
167 ap.client.say(cmd)
168
169
170func update_locations():
171 var ap = global.get_node("Archipelago")
172 var gamedata = global.get_node("Gamedata")
173
174 tracker_label.clear()
175 tracker_label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
176 tracker_label.push_font_size(24)
177
178 locations_overlay.clear()
179 locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf"))
180 locations_overlay.push_font_size(24)
181 locations_overlay.push_color(Color(0.9, 0.9, 0.9, 1))
182 locations_overlay.push_outline_color(Color(0, 0, 0, 1))
183 locations_overlay.push_outline_size(2)
184
185 const kLocation = 0
186 const kWorldport = 1
187 const kGoal = 2
188
189 var location_names = []
190 var type_by_name = {}
191 for location_id in ap.client._accessible_locations:
192 if not ap.client._checked_locations.has(location_id):
193 var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)")
194 location_names.append(location_name)
195 type_by_name[location_name] = kLocation
196
197 for port_id in ap.client._accessible_worldports:
198 if not ap.client._checked_worldports.has(port_id):
199 var port_name = gamedata.get_worldport_display_name(port_id)
200 location_names.append(port_name)
201 type_by_name[port_name] = kWorldport
202
203 location_names.sort()
204
205 if ap.client._goal_accessible:
206 var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
207 ap.victory_condition
208 ]]
209 location_names.push_front(location_name)
210 type_by_name[location_name] = kGoal
211
212 var count = 0
213 for location_name in location_names:
214 tracker_label.append_text("[p]%s[/p]" % location_name)
215 if count < 18:
216 locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT)
217 locations_overlay.append_text(location_name)
218 locations_overlay.append_text(" ")
219 if type_by_name[location_name] == kLocation:
220 locations_overlay.add_image(location_texture)
221 elif type_by_name[location_name] == kWorldport:
222 locations_overlay.add_image(worldport_texture)
223 elif type_by_name[location_name] == kGoal:
224 locations_overlay.add_image(goal_texture)
225 locations_overlay.pop()
226 count += 1
227
228 if count > 18:
229 locations_overlay.append_text("[p align=right][lb]...[rb][/p]")
230
231
232func update_locations_visibility():
233 var ap = global.get_node("Archipelago")
234 locations_overlay.visible = ap.show_locations
235
236
237func setup_worldports():
238 tabs.set_tab_hidden(2, false)
239
240 var root_ti = worldports_tree.create_item(null)
241
242 var ports_by_map_id = {}
243 var display_names_by_map_id = {}
244 var display_names_by_port_id = {}
245
246 var ap = global.get_node("Archipelago")
247 var gamedata = global.get_node("Gamedata")
248 for fpid in ap.port_pairings:
249 var port = gamedata.objects.get_ports()[fpid]
250 var room = gamedata.objects.get_rooms()[port.get_room_id()]
251
252 if not ports_by_map_id.has(room.get_map_id()):
253 ports_by_map_id[room.get_map_id()] = []
254
255 var map = gamedata.objects.get_maps()[room.get_map_id()]
256 display_names_by_map_id[map.get_id()] = map.get_display_name()
257
258 ports_by_map_id[room.get_map_id()].append(fpid)
259 display_names_by_port_id[fpid] = port.get_display_name()
260
261 var sorted_map_ids = ports_by_map_id.keys().duplicate()
262 sorted_map_ids.sort_custom(
263 func(a, b): return display_names_by_map_id[a] < display_names_by_map_id[b]
264 )
265
266 for map_id in sorted_map_ids:
267 var map_ti = root_ti.create_child()
268 map_ti.set_text(0, display_names_by_map_id[map_id])
269 map_ti.visible = false
270 map_ti.collapsed = true
271 port_tree_item_by_map[map_id] = map_ti
272 port_tree_item_by_map_port[map_id] = {}
273
274 var port_ids = ports_by_map_id[map_id]
275 port_ids.sort_custom(
276 func(a, b): return display_names_by_port_id[a] < display_names_by_port_id[b]
277 )
278
279 for port_id in port_ids:
280 var port_ti = map_ti.create_child()
281 port_ti.set_text(0, display_names_by_port_id[port_id])
282 port_ti.set_text(1, gamedata.get_worldport_display_name(ap.port_pairings[port_id]))
283 port_ti.visible = false
284 port_tree_item_by_map_port[map_id][port_id] = port_ti
285
286 update_worldports()
287
288
289func update_worldports():
290 var ap = global.get_node("Archipelago")
291
292 for map_id in port_tree_item_by_map_port.keys():
293 var map_visible = false
294
295 for port_id in port_tree_item_by_map_port[map_id].keys():
296 var ti = port_tree_item_by_map_port[map_id][port_id]
297 ti.visible = ap.client._checked_worldports.has(port_id)
298
299 if ti.visible:
300 map_visible = true
301
302 port_tree_item_by_map[map_id].visible = map_visible
303
304
305func reset():
306 locations_overlay.clear()
307 tabs.set_tab_hidden(2, true)
308 port_tree_item_by_map.clear()
309 port_tree_item_by_map_port.clear()
310 worldports_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..5c2faff --- /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 == "menus/credits":
6 return
7
8 super.handleTriggered()