about summary refs log tree commit diff stats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/Archipelago/animationListener.gd14
-rw-r--r--client/Archipelago/client.gd63
-rw-r--r--client/Archipelago/collectable.gd16
-rw-r--r--client/Archipelago/door.gd14
-rw-r--r--client/Archipelago/gamedata.gd62
-rw-r--r--client/Archipelago/keyHolder.gd38
-rw-r--r--client/Archipelago/keyHolderChecker.gd24
-rw-r--r--client/Archipelago/keyHolderResetterListener.gd8
-rw-r--r--client/Archipelago/keyboard.gd199
-rw-r--r--client/Archipelago/manager.gd340
-rw-r--r--client/Archipelago/messages.gd18
-rw-r--r--client/Archipelago/painting.gd14
-rw-r--r--client/Archipelago/panel.gd101
-rw-r--r--client/Archipelago/pauseMenu.gd7
-rw-r--r--client/Archipelago/player.gd190
-rw-r--r--client/Archipelago/saver.gd6
-rw-r--r--client/Archipelago/settings_screen.gd78
-rw-r--r--client/Archipelago/teleport.gd38
-rw-r--r--client/Archipelago/teleportListener.gd23
-rw-r--r--client/Archipelago/textclient.gd6
-rw-r--r--client/Archipelago/victoryListener.gd20
-rw-r--r--client/Archipelago/visibilityListener.gd38
-rw-r--r--client/Archipelago/worldport.gd10
-rw-r--r--client/Archipelago/worldportListener.gd4
-rw-r--r--client/CHANGELOG.md38
-rw-r--r--client/README.md90
-rw-r--r--client/archipelago.tscn5
27 files changed, 1343 insertions, 121 deletions
diff --git a/client/Archipelago/animationListener.gd b/client/Archipelago/animationListener.gd index f1fb5fb..c3b26db 100644 --- a/client/Archipelago/animationListener.gd +++ b/client/Archipelago/animationListener.gd
@@ -1,6 +1,7 @@
1extends "res://scripts/nodes/listeners/animationListener.gd" 1extends "res://scripts/nodes/listeners/animationListener.gd"
2 2
3var item_id 3var item_id
4var item_amount
4 5
5 6
6func _ready(): 7func _ready():
@@ -8,17 +9,16 @@ func _ready():
8 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() 9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
9 ) 10 )
10 11
11 print("node: %s" % node_path)
12
13 var gamedata = global.get_node("Gamedata") 12 var gamedata = global.get_node("Gamedata")
14 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
15 if door_id != null: 14 if door_id != null:
16 print("door_id: %d" % door_id)
17
18 var ap = global.get_node("Archipelago") 15 var ap = global.get_node("Archipelago")
19 item_id = ap.get_item_id_for_door(door_id) 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]
20 21
21 if item_id != null:
22 self.senders = [] 22 self.senders = []
23 self.senderGroup = [] 23 self.senderGroup = []
24 self.nested = false 24 self.nested = false
@@ -34,5 +34,5 @@ func _ready():
34func _readier(): 34func _readier():
35 var ap = global.get_node("Archipelago") 35 var ap = global.get_node("Archipelago")
36 36
37 if ap.has_item(item_id): 37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered() 38 handleTriggered()
diff --git a/client/Archipelago/client.gd b/client/Archipelago/client.gd index 4c34e91..843647d 100644 --- a/client/Archipelago/client.gd +++ b/client/Archipelago/client.gd
@@ -32,17 +32,23 @@ var _players = []
32var _player_name_by_slot = {} 32var _player_name_by_slot = {}
33var _game_by_player = {} 33var _game_by_player = {}
34var _checked_locations = [] 34var _checked_locations = []
35var _received_items = [] 35var _received_indexes = []
36var _received_items = {}
36var _slot_data = {} 37var _slot_data = {}
37 38
38signal could_not_connect 39signal could_not_connect
39signal connect_status 40signal connect_status
40signal client_connected 41signal client_connected(slot_data)
41signal item_received(item_id, index, player, flags) 42signal item_received(item_id, index, player, flags, amount)
42signal message_received(message) 43signal message_received(message)
44signal location_scout_received(item_id, location_id, player, flags)
43 45
44 46
45func _init(): 47func _init():
48 set_process_mode(Node.PROCESS_MODE_ALWAYS)
49
50 _ws.inbound_buffer_size = 8388608
51
46 global._print("Instantiated APClient") 52 global._print("Instantiated APClient")
47 53
48 # Read AP datapackages from file, if there are any 54 # Read AP datapackages from file, if there are any
@@ -74,6 +80,8 @@ func _reset_state():
74 _authenticated = false 80 _authenticated = false
75 _try_wss = false 81 _try_wss = false
76 _has_connected = false 82 _has_connected = false
83 _received_items = {}
84 _received_indexes = []
77 85
78 86
79func _errored(): 87func _errored():
@@ -183,7 +191,7 @@ func _process(_delta):
183 player["slot"] 191 player["slot"]
184 )]["game"] 192 )]["game"]
185 193
186 emit_signal("client_connected") 194 emit_signal("client_connected", _slot_data)
187 195
188 elif cmd == "ConnectionRefused": 196 elif cmd == "ConnectionRefused":
189 var error_message = "" 197 var error_message = ""
@@ -219,7 +227,7 @@ func _process(_delta):
219 error_message = "Unknown error." 227 error_message = "Unknown error."
220 228
221 _initiated_disconnect = true 229 _initiated_disconnect = true
222 _ws.disconnect_from_host() 230 _ws.close()
223 231
224 emit_signal("could_not_connect", error_message) 232 emit_signal("could_not_connect", error_message)
225 global._print("Connection to AP refused") 233 global._print("Connection to AP refused")
@@ -228,21 +236,40 @@ func _process(_delta):
228 elif cmd == "ReceivedItems": 236 elif cmd == "ReceivedItems":
229 var i = 0 237 var i = 0
230 for item in message["items"]: 238 for item in message["items"]:
231 if not _received_items.has(int(item["item"])): 239 var index = int(message["index"] + i)
232 _received_items.append(int(item["item"])) 240 i += 1
241
242 if _received_indexes.has(index):
243 # Do not re-process items.
244 continue
245
246 _received_indexes.append(index)
247
248 var item_id = int(item["item"])
249 _received_items[item_id] = _received_items.get(item_id, 0) + 1
233 250
234 emit_signal( 251 emit_signal(
235 "item_received", 252 "item_received",
236 int(item["item"]), 253 item_id,
237 int(message["index"]) + i, 254 index,
238 int(item["player"]), 255 int(item["player"]),
239 int(item["flags"]) 256 int(item["flags"]),
257 _received_items[item_id]
240 ) 258 )
241 i += 1
242 259
243 elif cmd == "PrintJSON": 260 elif cmd == "PrintJSON":
244 emit_signal("message_received", message) 261 emit_signal("message_received", message)
245 262
263 elif cmd == "LocationInfo":
264 for loc in message["locations"]:
265 emit_signal(
266 "location_scout_received",
267 int(loc["item"]),
268 int(loc["location"]),
269 int(loc["player"]),
270 int(loc["flags"])
271 )
272
246 elif state == WebSocketPeer.STATE_CLOSED: 273 elif state == WebSocketPeer.STATE_CLOSED:
247 if _has_connected: 274 if _has_connected:
248 _closed() 275 _closed()
@@ -284,7 +311,7 @@ func connectToServer(server, un, pw):
284 % err 311 % err
285 ) 312 )
286 ) 313 )
287 global._print("Could not connect to AP: " + err) 314 global._print("Could not connect to AP: %d" % err)
288 return 315 return
289 _should_process = true 316 _should_process = true
290 317
@@ -353,6 +380,10 @@ func sendLocation(loc_id):
353 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}]) 380 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
354 381
355 382
383func sendLocations(loc_ids):
384 sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}])
385
386
356func setValue(key, value, operation = "replace"): 387func setValue(key, value, operation = "replace"):
357 sendMessage( 388 sendMessage(
358 [ 389 [
@@ -374,5 +405,13 @@ func completedGoal():
374 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL 405 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
375 406
376 407
408func scoutLocations(loc_ids):
409 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
410
411
377func hasItem(item_id): 412func hasItem(item_id):
378 return _received_items.has(item_id) 413 return _received_items.has(item_id)
414
415
416func getItemAmount(item_id):
417 return _received_items.get(item_id, 0)
diff --git a/client/Archipelago/collectable.gd b/client/Archipelago/collectable.gd new file mode 100644 index 0000000..4a17a2a --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/door.gd b/client/Archipelago/door.gd index 731eca4..fead818 100644 --- a/client/Archipelago/door.gd +++ b/client/Archipelago/door.gd
@@ -1,6 +1,7 @@
1extends "res://scripts/nodes/door.gd" 1extends "res://scripts/nodes/door.gd"
2 2
3var item_id 3var item_id
4var item_amount
4 5
5 6
6func _ready(): 7func _ready():
@@ -8,17 +9,16 @@ func _ready():
8 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() 9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
9 ) 10 )
10 11
11 print("node: %s" % node_path)
12
13 var gamedata = global.get_node("Gamedata") 12 var gamedata = global.get_node("Gamedata")
14 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
15 if door_id != null: 14 if door_id != null:
16 print("door_id: %d" % door_id)
17
18 var ap = global.get_node("Archipelago") 15 var ap = global.get_node("Archipelago")
19 item_id = ap.get_item_id_for_door(door_id) 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]
20 21
21 if item_id != null:
22 self.senders = [] 22 self.senders = []
23 self.senderGroup = [] 23 self.senderGroup = []
24 self.nested = false 24 self.nested = false
@@ -34,5 +34,5 @@ func _ready():
34func _readier(): 34func _readier():
35 var ap = global.get_node("Archipelago") 35 var ap = global.get_node("Archipelago")
36 36
37 if ap.has_item(item_id): 37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered() 38 handleTriggered()
diff --git a/client/Archipelago/gamedata.gd b/client/Archipelago/gamedata.gd index 16368a9..41d966a 100644 --- a/client/Archipelago/gamedata.gd +++ b/client/Archipelago/gamedata.gd
@@ -5,13 +5,42 @@ var SCRIPT_proto
5var objects 5var objects
6var door_id_by_map_node_path = {} 6var door_id_by_map_node_path = {}
7var painting_id_by_map_node_path = {} 7var painting_id_by_map_node_path = {}
8var panel_id_by_map_node_path = {}
8var door_id_by_ap_id = {} 9var door_id_by_ap_id = {}
9var map_id_by_name = {} 10var map_id_by_name = {}
11var progressive_id_by_ap_id = {}
12var letter_id_by_ap_id = {}
13var symbol_item_ids = []
14var anti_trap_ids = {}
15
16var kSYMBOL_ITEMS
10 17
11 18
12func _init(proto_script): 19func _init(proto_script):
13 SCRIPT_proto = proto_script 20 SCRIPT_proto = proto_script
14 21
22 kSYMBOL_ITEMS = {
23 SCRIPT_proto.PuzzleSymbol.SUN: "Sun Symbol",
24 SCRIPT_proto.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
25 SCRIPT_proto.PuzzleSymbol.ZERO: "Zero Symbol",
26 SCRIPT_proto.PuzzleSymbol.EXAMPLE: "Example Symbol",
27 SCRIPT_proto.PuzzleSymbol.BOXES: "Boxes Symbol",
28 SCRIPT_proto.PuzzleSymbol.PLANET: "Planet Symbol",
29 SCRIPT_proto.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
30 SCRIPT_proto.PuzzleSymbol.CROSS: "Cross Symbol",
31 SCRIPT_proto.PuzzleSymbol.SWEET: "Sweet Symbol",
32 SCRIPT_proto.PuzzleSymbol.GENDER: "Gender Symbol",
33 SCRIPT_proto.PuzzleSymbol.AGE: "Age Symbol",
34 SCRIPT_proto.PuzzleSymbol.SOUND: "Sound Symbol",
35 SCRIPT_proto.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
36 SCRIPT_proto.PuzzleSymbol.JOB: "Job Symbol",
37 SCRIPT_proto.PuzzleSymbol.STARS: "Stars Symbol",
38 SCRIPT_proto.PuzzleSymbol.NULL: "Null Symbol",
39 SCRIPT_proto.PuzzleSymbol.EVAL: "Eval Symbol",
40 SCRIPT_proto.PuzzleSymbol.LINGO: "Lingo Symbol",
41 SCRIPT_proto.PuzzleSymbol.QUESTION: "Question Symbol",
42 }
43
15 44
16func load(data_bytes): 45func load(data_bytes):
17 objects = SCRIPT_proto.AllObjects.new() 46 objects = SCRIPT_proto.AllObjects.new()
@@ -50,6 +79,31 @@ func load(data_bytes):
50 79
51 var _map_data = painting_id_by_map_node_path[map.get_name()] 80 var _map_data = painting_id_by_map_node_path[map.get_name()]
52 81
82 for progressive in objects.get_progressives():
83 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
84
85 for letter in objects.get_letters():
86 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
87
88 for panel in objects.get_panels():
89 var room = objects.get_rooms()[panel.get_room_id()]
90 var map = objects.get_maps()[room.get_map_id()]
91
92 if not map.get_name() in panel_id_by_map_node_path:
93 panel_id_by_map_node_path[map.get_name()] = {}
94
95 var map_data = panel_id_by_map_node_path[map.get_name()]
96 map_data[panel.get_path()] = panel.get_id()
97
98 for symbol_name in kSYMBOL_ITEMS.values():
99 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
100
101 for special_name in objects.get_special_ids().keys():
102 if special_name.begins_with("Anti "):
103 anti_trap_ids[objects.get_special_ids()[special_name]] = (
104 special_name.substr(5).to_lower()
105 )
106
53 107
54func get_door_for_map_node_path(map_name, node_path): 108func get_door_for_map_node_path(map_name, node_path):
55 if not door_id_by_map_node_path.has(map_name): 109 if not door_id_by_map_node_path.has(map_name):
@@ -59,6 +113,14 @@ func get_door_for_map_node_path(map_name, node_path):
59 return map_data.get(node_path, null) 113 return map_data.get(node_path, null)
60 114
61 115
116func get_panel_for_map_node_path(map_name, node_path):
117 if not panel_id_by_map_node_path.has(map_name):
118 return null
119
120 var map_data = panel_id_by_map_node_path[map_name]
121 return map_data.get(node_path, null)
122
123
62func get_door_ap_id(door_id): 124func get_door_ap_id(door_id):
63 var door = objects.get_doors()[door_id] 125 var door = objects.get_doors()[door_id]
64 if door.has_ap_id(): 126 if door.has_ap_id():
diff --git a/client/Archipelago/keyHolder.gd b/client/Archipelago/keyHolder.gd new file mode 100644 index 0000000..3c037ff --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/keyHolderChecker.gd b/client/Archipelago/keyHolderChecker.gd new file mode 100644 index 0000000..a75a9e4 --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/keyHolderResetterListener.gd b/client/Archipelago/keyHolderResetterListener.gd new file mode 100644 index 0000000..d5300f3 --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/keyboard.gd b/client/Archipelago/keyboard.gd new file mode 100644 index 0000000..450566d --- /dev/null +++ b/client/Archipelago/keyboard.gd
@@ -0,0 +1,199 @@
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 for k in kALL_LETTERS:
52 var level = 0
53
54 if ap.get_letter_behavior(k, false) == ap.kLETTER_BEHAVIOR_UNLOCKED:
55 level += 1
56 if ap.get_letter_behavior(k, true) == ap.kLETTER_BEHAVIOR_UNLOCKED:
57 level += 1
58
59 letters_dynamic[k] = level
60
61 update_unlocks()
62
63
64func save():
65 var dir = DirAccess.open("user://")
66 var folder = "archipelago_keys"
67 if not dir.dir_exists(folder):
68 dir.make_dir(folder)
69
70 var file = FileAccess.open(filename, FileAccess.WRITE)
71
72 var data = [
73 letters_saved,
74 letters_in_keyholders,
75 keyholder_state,
76 ]
77 file.store_var(data, true)
78 file.close()
79
80
81func update_unlocks():
82 unlocks.resetKeys()
83
84 var has_doubles = false
85
86 for k in kALL_LETTERS:
87 var level = 0
88
89 if not letters_in_keyholders.has(k):
90 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0)
91
92 if level >= 2:
93 level = 2
94 has_doubles = true
95
96 if letters_blocked.has(k):
97 level = 0
98
99 unlocks.unlockKey(k, level)
100
101 if has_doubles and unlocks.data["double_letters"] != "unlocked":
102 var ap = global.get_node("Archipelago")
103 if ap.cyan_door_behavior == ap.kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER:
104 unlocks.setData("double_letters", "unlocked")
105
106
107func collect_local_letter(key, level):
108 if level < 0 or level > 2 or level < letters_saved.get(key, 0):
109 return
110
111 letters_saved[key] = level
112
113 if letters_blocked.has(key):
114 letters_blocked.erase(key)
115
116 update_unlocks()
117 save()
118
119
120func collect_remote_letter(key, level):
121 if level < 0 or level > 2 or level < letters_dynamic.get(key, 0):
122 return
123
124 letters_dynamic[key] = level
125
126 if letters_blocked.has(key):
127 letters_blocked.erase(key)
128
129 update_unlocks()
130 save()
131
132
133func put_in_keyholder(key, map, kh_path):
134 if not keyholder_state.has(map):
135 keyholder_state[map] = {}
136
137 keyholder_state[map][kh_path] = key
138 letters_in_keyholders.append(key)
139
140 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(
141 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
142 )
143
144 update_unlocks()
145 save()
146
147
148func remove_from_keyholder(key, map, kh_path):
149 if not keyholder_state.has(map):
150 # This... shouldn't happen.
151 keyholder_state[map] = {}
152
153 keyholder_state[map].erase(kh_path)
154 letters_in_keyholders.erase(key)
155
156 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(key, 0)
157
158 update_unlocks()
159 save()
160
161
162func block_letter(key):
163 if not letters_blocked.has(key):
164 letters_blocked.append(key)
165
166 update_unlocks()
167
168
169func load_keyholders(map):
170 if keyholder_state.has(map):
171 var khs = keyholder_state[map]
172
173 for path in khs.keys():
174 var key = khs[path]
175 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
176 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
177 )
178
179
180func reset_keyholders():
181 if letters_in_keyholders.is_empty() and letters_blocked.is_empty():
182 return false
183
184 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty()
185
186 if keyholder_state.has(global.map):
187 for path in keyholder_state[global.map]:
188 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
189 keyholder_state[global.map][path], 0
190 )
191
192 keyholder_state.clear()
193 letters_in_keyholders.clear()
194 letters_blocked.clear()
195
196 update_unlocks()
197 save()
198
199 return cleared_anything
diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd index 10c4096..20333e5 100644 --- a/client/Archipelago/manager.gd +++ b/client/Archipelago/manager.gd
@@ -1,10 +1,12 @@
1extends Node 1extends Node
2 2
3const my_version = "0.1.0" 3const MOD_VERSION = 5
4 4
5var SCRIPT_client 5var SCRIPT_client
6var SCRIPT_keyboard
6var SCRIPT_locationListener 7var SCRIPT_locationListener
7var SCRIPT_uuid 8var SCRIPT_uuid
9var SCRIPT_victoryListener
8 10
9var ap_server = "" 11var ap_server = ""
10var ap_user = "" 12var ap_user = ""
@@ -12,10 +14,43 @@ var ap_pass = ""
12var connection_history = [] 14var connection_history = []
13 15
14var client 16var client
17var keyboard
15 18
16var _localdata_file = "" 19var _localdata_file = ""
17var _received_indexes = []
18var _last_new_item = -1 20var _last_new_item = -1
21var _batch_locations = false
22var _held_locations = []
23var _held_location_scouts = []
24var _location_scouts = {}
25var _item_locks = {}
26var _inverse_item_locks = {}
27var _held_letters = {}
28var _letters_setup = false
29
30const kSHUFFLE_LETTERS_VANILLA = 0
31const kSHUFFLE_LETTERS_UNLOCKED = 1
32const kSHUFFLE_LETTERS_PROGRESSIVE = 2
33const kSHUFFLE_LETTERS_VANILLA_CYAN = 3
34const kSHUFFLE_LETTERS_ITEM_CYAN = 4
35
36const kLETTER_BEHAVIOR_VANILLA = 0
37const kLETTER_BEHAVIOR_ITEM = 1
38const kLETTER_BEHAVIOR_UNLOCKED = 2
39
40const kCYAN_DOOR_BEHAVIOR_H2 = 0
41const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
42const kCYAN_DOOR_BEHAVIOR_ITEM = 2
43
44var apworld_version = [0, 0]
45var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
46var daedalus_roof_access = false
47var keyholder_sanity = false
48var shuffle_control_center_colors = false
49var shuffle_doors = false
50var shuffle_gallery_paintings = false
51var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
52var shuffle_symbols = false
53var victory_condition = -1
19 54
20signal could_not_connect 55signal could_not_connect
21signal connect_status 56signal connect_status
@@ -52,12 +87,16 @@ func _ready():
52 87
53 client.connect("item_received", _process_item) 88 client.connect("item_received", _process_item)
54 client.connect("message_received", _process_message) 89 client.connect("message_received", _process_message)
90 client.connect("location_scout_received", _process_location_scout)
55 client.connect("could_not_connect", _client_could_not_connect) 91 client.connect("could_not_connect", _client_could_not_connect)
56 client.connect("connect_status", _client_connect_status) 92 client.connect("connect_status", _client_connect_status)
57 client.connect("client_connected", _client_connected) 93 client.connect("client_connected", _client_connected)
58 94
59 add_child(client) 95 add_child(client)
60 96
97 keyboard = SCRIPT_keyboard.new()
98 add_child(keyboard)
99
61 100
62func saveSettings(): 101func saveSettings():
63 # Save the AP settings to disk. 102 # Save the AP settings to disk.
@@ -91,8 +130,13 @@ func saveLocaldata():
91 130
92 131
93func connectToServer(): 132func connectToServer():
94 _received_indexes = []
95 _last_new_item = -1 133 _last_new_item = -1
134 _batch_locations = false
135 _held_locations = []
136 _held_location_scouts = []
137 _location_scouts = {}
138 _letters_setup = false
139 _held_letters = {}
96 140
97 client.connectToServer(ap_server, ap_user, ap_pass) 141 client.connectToServer(ap_server, ap_user, ap_pass)
98 142
@@ -106,48 +150,46 @@ func disconnect_from_ap():
106 150
107 151
108func get_item_id_for_door(door_id): 152func get_item_id_for_door(door_id):
109 var gamedata = global.get_node("Gamedata") 153 return _item_locks.get(door_id, null)
110 var door = gamedata.objects.get_doors()[door_id]
111 if (
112 door.get_type() == gamedata.SCRIPT_proto.DoorType.EVENT
113 or door.get_type() == gamedata.SCRIPT_proto.DoorType.LOCATION_ONLY
114 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
115 ):
116 return null
117 return gamedata.get_door_ap_id(door_id)
118
119
120func has_item(item_id):
121 return client.hasItem(item_id)
122
123 154
124func _process_item(item, index, from, flags):
125 if index != null:
126 if _received_indexes.has(index):
127 # Do not re-process items.
128 return
129
130 _received_indexes.append(index)
131 155
156func _process_item(item, index, from, flags, amount):
132 var item_name = "Unknown" 157 var item_name = "Unknown"
133 if client._item_id_to_name["Lingo 2"].has(item): 158 if client._item_id_to_name["Lingo 2"].has(item):
134 item_name = client._item_id_to_name["Lingo 2"][item] 159 item_name = client._item_id_to_name["Lingo 2"][item]
135 160
136 var gamedata = global.get_node("Gamedata") 161 var gamedata = global.get_node("Gamedata")
137 var door_id = gamedata.door_id_by_ap_id.get(item, null) 162
138 if door_id != null and gamedata.get_door_map_name(door_id) == global.map: 163 var prog_id = null
139 var receivers = gamedata.get_door_receivers(door_id) 164 if _inverse_item_locks.has(item):
140 var scene = get_tree().get_root().get_node_or_null("scene") 165 for lock in _inverse_item_locks.get(item):
141 if scene != null: 166 if lock[1] != amount:
142 for receiver in receivers: 167 continue
143 var rnode = scene.get_node_or_null(receiver) 168
144 if rnode != null: 169 if gamedata.progressive_id_by_ap_id.has(item):
145 rnode.handleTriggered() 170 prog_id = lock[0]
146 #for painting_id in gamedata.objects.get_doors()[door_id].get_move_paintings(): 171
147 # var painting = gamedata.objects.get_paintings()[painting_id] 172 if gamedata.get_door_map_name(lock[0]) != global.map:
148 # var pnode = scene.get_node_or_null(painting.get_path() + "/teleportListener") 173 continue
149 # if pnode != null: 174
150 # pnode.handleTriggered() 175 var receivers = gamedata.get_door_receivers(lock[0])
176 var scene = get_tree().get_root().get_node_or_null("scene")
177 if scene != null:
178 for receiver in receivers:
179 var rnode = scene.get_node_or_null(receiver)
180 if rnode != null:
181 rnode.handleTriggered()
182
183 var letter_id = gamedata.letter_id_by_ap_id.get(item, null)
184 if letter_id != null:
185 var letter = gamedata.objects.get_letters()[letter_id]
186 if not letter.has_level2() or not letter.get_level2():
187 _process_key_item(letter.get_key(), amount)
188
189 if gamedata.symbol_item_ids.has(item):
190 var player = get_tree().get_root().get_node_or_null("scene/player")
191 if player != null:
192 player.emit_signal("evaluate_solvability")
151 193
152 # Show a message about the item if it's new. 194 # Show a message about the item if it's new.
153 if index != null and index > _last_new_item: 195 if index != null and index > _last_new_item:
@@ -155,16 +197,26 @@ func _process_item(item, index, from, flags):
155 saveLocaldata() 197 saveLocaldata()
156 198
157 var player_name = "Unknown" 199 var player_name = "Unknown"
158 if client._player_name_by_slot.has(from): 200 if client._player_name_by_slot.has(float(from)):
159 player_name = client._player_name_by_slot[from] 201 player_name = client._player_name_by_slot[float(from)]
160 202
161 var item_color = colorForItemType(flags) 203 var item_color = colorForItemType(flags)
162 204
205 var full_item_name = item_name
206 if prog_id != null:
207 var door = gamedata.objects.get_doors()[prog_id]
208 full_item_name = "%s (%s)" % [item_name, door.get_name()]
209
163 var message 210 var message
164 if from == client._slot: 211 if from == client._slot:
165 message = "Found [color=%s]%s[/color]" % [item_color, item_name] 212 message = "Found [color=%s]%s[/color]" % [item_color, full_item_name]
166 else: 213 else:
167 message = "Received [color=%s]%s[/color] from %s" % [item_color, item_name, player_name] 214 message = (
215 "Received [color=%s]%s[/color] from %s" % [item_color, full_item_name, player_name]
216 )
217
218 if gamedata.anti_trap_ids.has(item):
219 keyboard.block_letter(gamedata.anti_trap_ids[item])
168 220
169 global._print(message) 221 global._print(message)
170 222
@@ -183,15 +235,15 @@ func _process_message(message):
183 235
184 var item_name = "Unknown" 236 var item_name = "Unknown"
185 var item_player_game = client._game_by_player[message["receiving"]] 237 var item_player_game = client._game_by_player[message["receiving"]]
186 if client._item_id_to_name[item_player_game].has(message["item"]["item"]): 238 if client._item_id_to_name[item_player_game].has(int(message["item"]["item"])):
187 item_name = client._item_id_to_name[item_player_game][message["item"]["item"]] 239 item_name = client._item_id_to_name[item_player_game][int(message["item"]["item"])]
188 240
189 var location_name = "Unknown" 241 var location_name = "Unknown"
190 var location_player_game = client._game_by_player[message["item"]["player"]] 242 var location_player_game = client._game_by_player[message["item"]["player"]]
191 if client._location_id_to_name[location_player_game].has(message["item"]["location"]): 243 if client._location_id_to_name[location_player_game].has(int(message["item"]["location"])):
192 location_name = ( 244 location_name = (client._location_id_to_name[location_player_game][int(
193 client._location_id_to_name[location_player_game][message["item"]["location"]] 245 message["item"]["location"]
194 ) 246 )])
195 247
196 var player_name = "Unknown" 248 var player_name = "Unknown"
197 if client._player_name_by_slot.has(message["receiving"]): 249 if client._player_name_by_slot.has(message["receiving"]):
@@ -257,22 +309,53 @@ func parse_printjson_for_textclient(message):
257 textclient_node.parse_printjson("".join(parts)) 309 textclient_node.parse_printjson("".join(parts))
258 310
259 311
260func _client_could_not_connect(): 312func _process_location_scout(item_id, location_id, player, flags):
261 emit_signal("could_not_connect") 313 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags}
314
315 if player == client._slot and flags & 4 != 0:
316 # This is a trap for us, so let's not display it.
317 return
318
319 var gamedata = global.get_node("Gamedata")
320 var map_id = gamedata.map_id_by_name.get(global.map)
321
322 var item_name = "Unknown"
323 var item_player_game = client._game_by_player[float(player)]
324 if client._item_id_to_name[item_player_game].has(item_id):
325 item_name = client._item_id_to_name[item_player_game][item_id]
326
327 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
328 if letter_id != null:
329 var letter = gamedata.objects.get_letters()[letter_id]
330 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
331 if room.get_map_id() == map_id:
332 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
333 letter.get_path()
334 )
335 if collectable != null:
336 collectable.setScoutedText(item_name)
337
338
339func _client_could_not_connect(message):
340 emit_signal("could_not_connect", message)
262 341
263 342
264func _client_connect_status(message): 343func _client_connect_status(message):
265 emit_signal("connect_status", message) 344 emit_signal("connect_status", message)
266 345
267 346
268func _client_connected(): 347func _client_connected(slot_data):
348 var gamedata = global.get_node("Gamedata")
349
269 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot] 350 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
270 _last_new_item = -1 351 _last_new_item = -1
271 352
272 if FileAccess.file_exists(_localdata_file): 353 if FileAccess.file_exists(_localdata_file):
273 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ) 354 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
274 var localdata = ap_file.get_var(true) 355 var localdata = []
275 ap_file.close() 356 if ap_file != null:
357 localdata = ap_file.get_var(true)
358 ap_file.close()
276 359
277 if typeof(localdata) != TYPE_ARRAY: 360 if typeof(localdata) != TYPE_ARRAY:
278 print("AP localdata file is corrupted") 361 print("AP localdata file is corrupted")
@@ -281,11 +364,113 @@ func _client_connected():
281 if localdata.size() > 0: 364 if localdata.size() > 0:
282 _last_new_item = localdata[0] 365 _last_new_item = localdata[0]
283 366
367 # Read slot data.
368 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
369 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
370 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
371 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
372 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
373 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
374 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
375 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
376 victory_condition = int(slot_data.get("victory_condition", 0))
377
378 if slot_data.has("version"):
379 apworld_version = [int(slot_data["version"][0]), int(slot_data["version"][1])]
380
381 # Set up item locks.
382 _item_locks = {}
383
384 if shuffle_doors:
385 for door in gamedata.objects.get_doors():
386 if (
387 door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD
388 or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
389 ):
390 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
391
392 for progressive in gamedata.objects.get_progressives():
393 for i in range(0, progressive.get_doors().size()):
394 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
395 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
396
397 for door_group in gamedata.objects.get_door_groups():
398 if (
399 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR
400 or door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP
401 ):
402 for door in door_group.get_doors():
403 _item_locks[door] = [door_group.get_ap_id(), 1]
404
405 if shuffle_control_center_colors:
406 for door in gamedata.objects.get_doors():
407 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
408 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
409
410 for door_group in gamedata.objects.get_door_groups():
411 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR:
412 for door in door_group.get_doors():
413 _item_locks[door] = [door_group.get_ap_id(), 1]
414
415 if shuffle_gallery_paintings:
416 for door in gamedata.objects.get_doors():
417 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
418 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
419
420 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
421 for door_group in gamedata.objects.get_door_groups():
422 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
423 for door in door_group.get_doors():
424 if not _item_locks.has(door):
425 _item_locks[door] = [door_group.get_ap_id(), 1]
426
427 # Create a reverse item locks map for processing items.
428 _inverse_item_locks = {}
429
430 for door_id in _item_locks.keys():
431 var lock = _item_locks.get(door_id)
432
433 if not _inverse_item_locks.has(lock[0]):
434 _inverse_item_locks[lock[0]] = []
435
436 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
437
284 emit_signal("ap_connected") 438 emit_signal("ap_connected")
285 439
286 440
441func start_batching_locations():
442 _batch_locations = true
443
444
287func send_location(loc_id): 445func send_location(loc_id):
288 client.sendLocation(loc_id) 446 if _batch_locations:
447 _held_locations.append(loc_id)
448 else:
449 client.sendLocation(loc_id)
450
451
452func scout_location(loc_id):
453 if _location_scouts.has(loc_id):
454 return _location_scouts.get(loc_id)
455
456 if _batch_locations:
457 _held_location_scouts.append(loc_id)
458 else:
459 client.scoutLocation(loc_id)
460
461 return null
462
463
464func stop_batching_locations():
465 _batch_locations = false
466
467 if not _held_locations.is_empty():
468 client.sendLocations(_held_locations)
469 _held_locations.clear()
470
471 if not _held_location_scouts.is_empty():
472 client.scoutLocations(_held_location_scouts)
473 _held_location_scouts.clear()
289 474
290 475
291func colorForItemType(flags): 476func colorForItemType(flags):
@@ -301,3 +486,50 @@ func colorForItemType(flags):
301 return "#d63a22" 486 return "#d63a22"
302 else: # filler 487 else: # filler
303 return "#14de9e" 488 return "#14de9e"
489
490
491func get_letter_behavior(key, level2):
492 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
493 return kLETTER_BEHAVIOR_UNLOCKED
494
495 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
496 if level2:
497 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
498 return kLETTER_BEHAVIOR_VANILLA
499 else:
500 return kLETTER_BEHAVIOR_ITEM
501 else:
502 return kLETTER_BEHAVIOR_UNLOCKED
503
504 if not level2 and ["h", "i", "n", "t"].has(key):
505 # This differs from the equivalent function in the apworld. Logically it is
506 # the same as UNLOCKED since they are in the starting room, but VANILLA
507 # means the player still has to actually pick up the letters.
508 return kLETTER_BEHAVIOR_VANILLA
509
510 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
511 return kLETTER_BEHAVIOR_ITEM
512
513 return kLETTER_BEHAVIOR_VANILLA
514
515
516func setup_keys():
517 keyboard.load_seed()
518
519 _letters_setup = true
520
521 for k in _held_letters.keys():
522 _process_key_item(k, _held_letters[k])
523
524 _held_letters.clear()
525
526
527func _process_key_item(key, level):
528 if not _letters_setup:
529 _held_letters[key] = max(_held_letters.get(key, 0), level)
530 return
531
532 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
533 level += 1
534
535 keyboard.collect_remote_letter(key, level)
diff --git a/client/Archipelago/messages.gd b/client/Archipelago/messages.gd index 52f38b9..82fdbc4 100644 --- a/client/Archipelago/messages.gd +++ b/client/Archipelago/messages.gd
@@ -48,10 +48,11 @@ func showMessage(text):
48 while !_ordered_labels.is_empty(): 48 while !_ordered_labels.is_empty():
49 await get_tree().create_timer(timeout).timeout 49 await get_tree().create_timer(timeout).timeout
50 50
51 var to_remove = _ordered_labels.pop_front() 51 if !_ordered_labels.is_empty():
52 var to_tween = get_tree().create_tween().bind_node(to_remove) 52 var to_remove = _ordered_labels.pop_front()
53 to_tween.tween_property(to_remove, "modulate:a", 0.0, 0.5) 53 var to_tween = get_tree().create_tween().bind_node(to_remove)
54 to_tween.tween_callback(to_remove.queue_free) 54 to_tween.tween_property(to_remove, "modulate:a", 0.0, 0.5)
55 to_tween.tween_callback(to_remove.queue_free)
55 56
56 if !_message_queue.is_empty(): 57 if !_message_queue.is_empty():
57 var next_msg = _message_queue.pop_front() 58 var next_msg = _message_queue.pop_front()
@@ -59,3 +60,12 @@ func showMessage(text):
59 60
60 if timeout > 4: 61 if timeout > 4:
61 timeout -= 3 62 timeout -= 3
63
64
65func clear():
66 _message_queue.clear()
67
68 for message_label in _ordered_labels:
69 message_label.queue_free()
70
71 _ordered_labels.clear()
diff --git a/client/Archipelago/painting.gd b/client/Archipelago/painting.gd index 6b3de0b..276d4eb 100644 --- a/client/Archipelago/painting.gd +++ b/client/Archipelago/painting.gd
@@ -1,6 +1,7 @@
1extends "res://scripts/nodes/painting.gd" 1extends "res://scripts/nodes/painting.gd"
2 2
3var item_id 3var item_id
4var item_amount
4 5
5 6
6func _ready(): 7func _ready():
@@ -8,17 +9,16 @@ func _ready():
8 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() 9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
9 ) 10 )
10 11
11 print("node: %s" % node_path)
12
13 var gamedata = global.get_node("Gamedata") 12 var gamedata = global.get_node("Gamedata")
14 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
15 if door_id != null: 14 if door_id != null:
16 print("door_id: %d" % door_id)
17
18 var ap = global.get_node("Archipelago") 15 var ap = global.get_node("Archipelago")
19 item_id = ap.get_item_id_for_door(door_id) 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]
20 21
21 if item_id != null:
22 self.senders = [] 22 self.senders = []
23 self.senderGroup = [] 23 self.senderGroup = []
24 self.nested = false 24 self.nested = false
@@ -34,5 +34,5 @@ func _ready():
34func _readier(): 34func _readier():
35 var ap = global.get_node("Archipelago") 35 var ap = global.get_node("Archipelago")
36 36
37 if ap.has_item(item_id): 37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered() 38 handleTriggered()
diff --git a/client/Archipelago/panel.gd b/client/Archipelago/panel.gd new file mode 100644 index 0000000..fdaaf0e --- /dev/null +++ b/client/Archipelago/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").connect(
33 "evaluate_solvability", 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/client/Archipelago/pauseMenu.gd b/client/Archipelago/pauseMenu.gd index 6c013a5..df4bfd1 100644 --- a/client/Archipelago/pauseMenu.gd +++ b/client/Archipelago/pauseMenu.gd
@@ -4,3 +4,10 @@ extends "res://scripts/ui/pauseMenu.gd"
4func _pause_game(): 4func _pause_game():
5 global.get_node("Textclient").dismiss() 5 global.get_node("Textclient").dismiss()
6 super._pause_game() 6 super._pause_game()
7
8
9func _main_menu():
10 global.loaded = false
11 global.get_node("Archipelago").disconnect_from_ap()
12 global.get_node("Messages").clear()
13 super._main_menu()
diff --git a/client/Archipelago/player.gd b/client/Archipelago/player.gd index 082aa64..f0b214f 100644 --- a/client/Archipelago/player.gd +++ b/client/Archipelago/player.gd
@@ -1,10 +1,33 @@
1extends "res://scripts/nodes/player.gd" 1extends "res://scripts/nodes/player.gd"
2 2
3const kEndingNameByVictoryValue = {
4 0: "GRAY",
5 1: "PURPLE",
6 2: "MINT",
7 3: "BLACK",
8 4: "BLUE",
9 5: "CYAN",
10 6: "RED",
11 7: "PLUM",
12 8: "ORANGE",
13 9: "GOLD",
14 10: "YELLOW",
15 11: "GREEN",
16 12: "WHITE",
17}
18
19signal evaluate_solvability
20
3 21
4func _ready(): 22func _ready():
23 var khl_script = load("res://scripts/nodes/keyHolderListener.gd")
24
5 var ap = global.get_node("Archipelago") 25 var ap = global.get_node("Archipelago")
6 var gamedata = global.get_node("Gamedata") 26 var gamedata = global.get_node("Gamedata")
7 27
28 ap.start_batching_locations()
29
30 # Set up door locations.
8 var map_id = gamedata.map_id_by_name.get(global.map) 31 var map_id = gamedata.map_id_by_name.get(global.map)
9 for door in gamedata.objects.get_doors(): 32 for door in gamedata.objects.get_doors():
10 if door.get_map_id() != map_id: 33 if door.get_map_id() != map_id:
@@ -13,7 +36,10 @@ func _ready():
13 if not door.has_ap_id(): 36 if not door.has_ap_id():
14 continue 37 continue
15 38
16 if door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY: 39 if (
40 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
41 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
42 ):
17 continue 43 continue
18 44
19 var locationListener = ap.SCRIPT_locationListener.new() 45 var locationListener = ap.SCRIPT_locationListener.new()
@@ -21,12 +47,39 @@ func _ready():
21 locationListener.name = "locationListener_%d" % door.get_ap_id() 47 locationListener.name = "locationListener_%d" % door.get_ap_id()
22 48
23 for panel_ref in door.get_panels(): 49 for panel_ref in door.get_panels():
24 # TODO: specific answers
25 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()] 50 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
26 locationListener.senders.append(NodePath("/root/scene/" + panel_data.get_path())) 51 var panel_path = panel_data.get_path()
52
53 if panel_ref.has_answer():
54 for proxy in panel_data.get_proxies():
55 if proxy.get_answer() == panel_ref.get_answer():
56 panel_path = proxy.get_path()
57 break
58
59 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
60
61 for keyholder_ref in door.get_keyholders():
62 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
63
64 var khl = khl_script.new()
65 khl.name = (
66 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
67 )
68 khl.answer = keyholder_ref.get_key()
69 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
70 get_parent().add_child.call_deferred(khl)
71
72 locationListener.senders.append(NodePath("../" + khl.name))
73
74 for sender in door.get_senders():
75 locationListener.senders.append(NodePath("/root/scene/" + sender))
76
77 if door.has_complete_at():
78 locationListener.complete_at = door.get_complete_at()
27 79
28 get_parent().add_child.call_deferred(locationListener) 80 get_parent().add_child.call_deferred(locationListener)
29 81
82 # Set up letter locations.
30 for letter in gamedata.objects.get_letters(): 83 for letter in gamedata.objects.get_letters():
31 var room = gamedata.objects.get_rooms()[letter.get_room_id()] 84 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
32 if room.get_map_id() != map_id: 85 if room.get_map_id() != map_id:
@@ -39,6 +92,27 @@ func _ready():
39 92
40 get_parent().add_child.call_deferred(locationListener) 93 get_parent().add_child.call_deferred(locationListener)
41 94
95 if (
96 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
97 != ap.kLETTER_BEHAVIOR_VANILLA
98 ):
99 var scout = ap.scout_location(letter.get_ap_id())
100 if (
101 scout != null
102 and not (scout["player"] == ap.client._slot and scout["flags"] & 4 != 0)
103 ):
104 var item_name = "Unknown"
105 var item_player_game = ap.client._game_by_player[float(scout["player"])]
106 if ap.client._item_id_to_name[item_player_game].has(scout["item"]):
107 item_name = ap.client._item_id_to_name[item_player_game][scout["item"]]
108
109 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
110 letter.get_path()
111 )
112 if collectable != null:
113 collectable.setScoutedText.call_deferred(item_name)
114
115 # Set up mastery locations.
42 for mastery in gamedata.objects.get_masteries(): 116 for mastery in gamedata.objects.get_masteries():
43 var room = gamedata.objects.get_rooms()[mastery.get_room_id()] 117 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
44 if room.get_map_id() != map_id: 118 if room.get_map_id() != map_id:
@@ -51,6 +125,7 @@ func _ready():
51 125
52 get_parent().add_child.call_deferred(locationListener) 126 get_parent().add_child.call_deferred(locationListener)
53 127
128 # Set up ending locations.
54 for ending in gamedata.objects.get_endings(): 129 for ending in gamedata.objects.get_endings():
55 var room = gamedata.objects.get_rooms()[ending.get_room_id()] 130 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
56 if room.get_map_id() != map_id: 131 if room.get_map_id() != map_id:
@@ -63,4 +138,113 @@ func _ready():
63 138
64 get_parent().add_child.call_deferred(locationListener) 139 get_parent().add_child.call_deferred(locationListener)
65 140
141 if kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
142 var victoryListener = ap.SCRIPT_victoryListener.new()
143 victoryListener.name = "victoryListener"
144 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
145
146 get_parent().add_child.call_deferred(victoryListener)
147
148 # Set up keyholder locations, in keyholder sanity.
149 if ap.keyholder_sanity:
150 for keyholder in gamedata.objects.get_keyholders():
151 if not keyholder.has_key():
152 continue
153
154 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
155 if room.get_map_id() != map_id:
156 continue
157
158 var locationListener = ap.SCRIPT_locationListener.new()
159 locationListener.location_id = keyholder.get_ap_id()
160 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
161
162 var khl = khl_script.new()
163 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
164 khl.answer = keyholder.get_key()
165 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
166 get_parent().add_child.call_deferred(khl)
167
168 locationListener.senders.append(NodePath("../" + khl.name))
169
170 get_parent().add_child.call_deferred(locationListener)
171
172 # Block off roof access in Daedalus.
173 if global.map == "daedalus" and not ap.daedalus_roof_access:
174 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
175 _set_up_invis_wall(51.5, 11, -17, 16, 10, 1)
176 _set_up_invis_wall(46, 10, -9.5, 1, 10, 10)
177 _set_up_invis_wall(67.5, 11, 17, 16, 10, 1)
178 _set_up_invis_wall(50.5, 11, 14, 10, 10, 1)
179 _set_up_invis_wall(39, 10, 18.5, 1, 10, 22)
180 _set_up_invis_wall(20, 15, 18.5, 1, 10, 16)
181 _set_up_invis_wall(11.5, 15, 3, 32, 10, 1)
182 _set_up_invis_wall(11.5, 16, -20, 14, 20, 1)
183 _set_up_invis_wall(14, 16, -26.5, 1, 20, 4)
184 _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25)
185 _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1)
186 _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1)
187 _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11)
188 _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11)
189
190 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
191 var warp_exit = warp_exit_prefab.instantiate()
192 warp_exit.name = "roof_access_blocker_warp_exit"
193 warp_exit.position = Vector3(58, 10, 0)
194 warp_exit.rotation_degrees.y = 90
195 get_parent().add_child.call_deferred(warp_exit)
196
197 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
198 var warp_enter = warp_enter_prefab.instantiate()
199 warp_enter.target = warp_exit
200 warp_enter.position = Vector3(76.5, 30, 1)
201 warp_enter.scale = Vector3(4, 1.5, 1)
202 warp_enter.rotation_degrees.y = 90
203 get_parent().add_child.call_deferred(warp_enter)
204
205 if global.map == "the_entry":
206 # Remove door behind X1.
207 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
208 door_node.handleTriggered()
209
210 # Display win condition.
211 var sign_prefab = preload("res://objects/nodes/sign.tscn")
212 var sign1 = sign_prefab.instantiate()
213 sign1.position = Vector3(-7, 5, -15.01)
214 sign1.text = "victory"
215 get_parent().add_child.call_deferred(sign1)
216
217 var sign2 = sign_prefab.instantiate()
218 sign2.position = Vector3(-7, 4, -15.01)
219 sign2.text = "%s ending" % kEndingNameByVictoryValue.get(ap.victory_condition, "?")
220
221 var sign2_color = kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
222 if sign2_color == "white":
223 sign2_color = "silver"
224
225 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
226 get_parent().add_child.call_deferred(sign2)
227
66 super._ready() 228 super._ready()
229
230 await get_tree().process_frame
231 await get_tree().process_frame
232
233 ap.stop_batching_locations()
234
235
236func _set_up_invis_wall(x, y, z, sx, sy, sz):
237 var prefab = preload("res://objects/nodes/block.tscn")
238 var newwall = prefab.instantiate()
239 newwall.position.x = x
240 newwall.position.y = y
241 newwall.position.z = z
242 newwall.scale.x = sz
243 newwall.scale.y = sy
244 newwall.scale.z = sx
245 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
246 newwall.visibility_range_end = 3
247 newwall.visibility_range_end_margin = 1
248 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
249 newwall.skeleton = ".."
250 get_parent().add_child.call_deferred(newwall)
diff --git a/client/Archipelago/saver.gd b/client/Archipelago/saver.gd index 7e788a8..0fba9e7 100644 --- a/client/Archipelago/saver.gd +++ b/client/Archipelago/saver.gd
@@ -2,4 +2,8 @@ extends "res://scripts/nodes/saver.gd"
2 2
3 3
4func levelLoaded(): 4func levelLoaded():
5 reload.call_deferred() 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()
diff --git a/client/Archipelago/settings_screen.gd b/client/Archipelago/settings_screen.gd index 3697466..140b4f4 100644 --- a/client/Archipelago/settings_screen.gd +++ b/client/Archipelago/settings_screen.gd
@@ -22,27 +22,32 @@ func _ready():
22 var ap_instance = ap_script.new() 22 var ap_instance = ap_script.new()
23 ap_instance.name = "Archipelago" 23 ap_instance.name = "Archipelago"
24 24
25 #apclient_instance.SCRIPT_doorControl = load("user://maps/Archipelago/doorControl.gd")
26 #apclient_instance.SCRIPT_effects = load("user://maps/Archipelago/effects.gd")
27 #apclient_instance.SCRIPT_location = load("user://maps/Archipelago/location.gd")
28 #apclient_instance.SCRIPT_mypainting = load("user://maps/Archipelago/mypainting.gd")
29 #apclient_instance.SCRIPT_panel = load("user://maps/Archipelago/panel.gd")
30 #apclient_instance.SCRIPT_textclient = load("user://maps/Archipelago/textclient.gd")
31
32 ap_instance.SCRIPT_client = load("user://maps/Archipelago/client.gd") 25 ap_instance.SCRIPT_client = load("user://maps/Archipelago/client.gd")
26 ap_instance.SCRIPT_keyboard = load("user://maps/Archipelago/keyboard.gd")
33 ap_instance.SCRIPT_locationListener = load("user://maps/Archipelago/locationListener.gd") 27 ap_instance.SCRIPT_locationListener = load("user://maps/Archipelago/locationListener.gd")
34 ap_instance.SCRIPT_uuid = load("user://maps/Archipelago/vendor/uuid.gd") 28 ap_instance.SCRIPT_uuid = load("user://maps/Archipelago/vendor/uuid.gd")
29 ap_instance.SCRIPT_victoryListener = load("user://maps/Archipelago/victoryListener.gd")
35 30
36 global.add_child(ap_instance) 31 global.add_child(ap_instance)
37 32
38 # Let's also inject any scripts we need to inject now. 33 # Let's also inject any scripts we need to inject now.
39 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/animationListener.gd")) 34 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/animationListener.gd"))
35 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/collectable.gd"))
40 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/door.gd")) 36 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/door.gd"))
37 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/keyHolder.gd"))
38 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/keyHolderChecker.gd"))
39 installScriptExtension(
40 ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd")
41 )
41 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd")) 42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/panel.gd"))
42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd")) 44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd")) 45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd")) 46 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd"))
47 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleport.gd"))
45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleportListener.gd")) 48 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleportListener.gd"))
49 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/visibilityListener.gd"))
50 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/worldport.gd"))
46 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/worldportListener.gd")) 51 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/worldportListener.gd"))
47 52
48 var proto_script = load("user://maps/Archipelago/generated/proto.gd") 53 var proto_script = load("user://maps/Archipelago/generated/proto.gd")
@@ -65,6 +70,7 @@ func _ready():
65 global.add_child(textclient_instance) 70 global.add_child(textclient_instance)
66 71
67 var ap = global.get_node("Archipelago") 72 var ap = global.get_node("Archipelago")
73 var gamedata = global.get_node("Gamedata")
68 ap.connect("ap_connected", connectionSuccessful) 74 ap.connect("ap_connected", connectionSuccessful)
69 ap.connect("could_not_connect", connectionUnsuccessful) 75 ap.connect("could_not_connect", connectionUnsuccessful)
70 ap.connect("connect_status", connectionStatus) 76 ap.connect("connect_status", connectionStatus)
@@ -88,13 +94,17 @@ func _ready():
88 history_box.get_popup().connect("id_pressed", historySelected) 94 history_box.get_popup().connect("id_pressed", historySelected)
89 95
90 # Show client version. 96 # Show client version.
91 $Panel/title.text = "ARCHIPELAGO (%s)" % ap.my_version 97 $Panel/title.text = "ARCHIPELAGO (%d.%d)" % [gamedata.objects.get_version(), ap.MOD_VERSION]
92 98
93 # Increase font size in text boxes. 99 # Increase font size in text boxes.
94 $Panel/server_box.add_theme_font_size_override("font_size", 36) 100 $Panel/server_box.add_theme_font_size_override("font_size", 36)
95 $Panel/player_box.add_theme_font_size_override("font_size", 36) 101 $Panel/player_box.add_theme_font_size_override("font_size", 36)
96 $Panel/password_box.add_theme_font_size_override("font_size", 36) 102 $Panel/password_box.add_theme_font_size_override("font_size", 36)
97 103
104 # Set up version mismatch dialog.
105 $Panel/VersionMismatch.connect("confirmed", startGame)
106 $Panel/VersionMismatch.get_cancel_button().pressed.connect(versionMismatchDeclined)
107
98 108
99# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd 109# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
100func installScriptExtension(childScript: Resource): 110func installScriptExtension(childScript: Resource):
@@ -124,6 +134,33 @@ func connectionStatus(message):
124 134
125func connectionSuccessful(): 135func connectionSuccessful():
126 var ap = global.get_node("Archipelago") 136 var ap = global.get_node("Archipelago")
137 var gamedata = global.get_node("Gamedata")
138
139 # Check for major version mismatch.
140 if ap.apworld_version[0] != gamedata.objects.get_version():
141 $Panel/AcceptDialog.exclusive = false
142
143 var popup = self.get_node("Panel/VersionMismatch")
144 popup.title = "Version Mismatch!"
145 popup.dialog_text = (
146 "This slot was generated using v%d.%d of the Lingo 2 apworld,\nwhich has a different major version than this client (v%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."
147 % [
148 ap.apworld_version[0],
149 ap.apworld_version[1],
150 gamedata.objects.get_version(),
151 ap.MOD_VERSION
152 ]
153 )
154 popup.exclusive = true
155 popup.popup_centered()
156
157 return
158
159 startGame()
160
161
162func startGame():
163 var ap = global.get_node("Archipelago")
127 164
128 # Save connection details 165 # Save connection details
129 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass] 166 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
@@ -140,21 +177,30 @@ func connectionSuccessful():
140 global.universe = "lingo" 177 global.universe = "lingo"
141 global.map = "the_entry" 178 global.map = "the_entry"
142 179
143 unlocks.resetKeys()
144 unlocks.resetCollectables() 180 unlocks.resetCollectables()
145 unlocks.resetData() 181 unlocks.resetData()
146 unlocks.loadKeys() 182
183 ap.setup_keys()
184
147 unlocks.loadCollectables() 185 unlocks.loadCollectables()
148 unlocks.loadData() 186 unlocks.loadData()
149 unlocks.unlockKey("capslock", 1) 187 unlocks.unlockKey("capslock", 1)
150 188
151 clearResourceCache("res://objects/meshes/gridDoor.tscn") 189 clearResourceCache("res://objects/meshes/gridDoor.tscn")
190 clearResourceCache("res://objects/nodes/collectable.tscn")
152 clearResourceCache("res://objects/nodes/door.tscn") 191 clearResourceCache("res://objects/nodes/door.tscn")
192 clearResourceCache("res://objects/nodes/keyHolder.tscn")
153 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn") 193 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn")
194 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
195 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
154 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") 196 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
197 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn")
155 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") 198 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
199 clearResourceCache("res://objects/nodes/panel.tscn")
156 clearResourceCache("res://objects/nodes/player.tscn") 200 clearResourceCache("res://objects/nodes/player.tscn")
157 clearResourceCache("res://objects/nodes/saver.tscn") 201 clearResourceCache("res://objects/nodes/saver.tscn")
202 clearResourceCache("res://objects/nodes/teleport.tscn")
203 clearResourceCache("res://objects/nodes/worldport.tscn")
158 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn") 204 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn")
159 205
160 var paintings_dir = DirAccess.open("res://objects/meshes/paintings") 206 var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
@@ -166,7 +212,7 @@ func connectionSuccessful():
166 clearResourceCache("res://objects/meshes/paintings/" + file_name) 212 clearResourceCache("res://objects/meshes/paintings/" + file_name)
167 file_name = paintings_dir.get_next() 213 file_name = paintings_dir.get_next()
168 214
169 switcher.switch_map("res://objects/scenes/the_entry.tscn") 215 switcher.switch_map.call_deferred("res://objects/scenes/the_entry.tscn")
170 216
171 217
172func connectionUnsuccessful(error_message): 218func connectionUnsuccessful(error_message):
@@ -179,6 +225,13 @@ func connectionUnsuccessful(error_message):
179 popup.get_ok_button().visible = true 225 popup.get_ok_button().visible = true
180 popup.popup_centered() 226 popup.popup_centered()
181 227
228 $Panel/connect_button.disabled = false
229
230
231func versionMismatchDeclined():
232 $Panel/AcceptDialog.hide()
233 $Panel/connect_button.disabled = false
234
182 235
183func historySelected(index): 236func historySelected(index):
184 var ap = global.get_node("Archipelago") 237 var ap = global.get_node("Archipelago")
@@ -190,5 +243,4 @@ func historySelected(index):
190 243
191 244
192func clearResourceCache(path): 245func clearResourceCache(path):
193 ResourceLoader.load_threaded_request(path, "", false, ResourceLoader.CACHE_MODE_REPLACE) 246 ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
194 ResourceLoader.load_threaded_get(path)
diff --git a/client/Archipelago/teleport.gd b/client/Archipelago/teleport.gd new file mode 100644 index 0000000..428d50b --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/teleportListener.gd b/client/Archipelago/teleportListener.gd index 4bb08c9..6f363af 100644 --- a/client/Archipelago/teleportListener.gd +++ b/client/Archipelago/teleportListener.gd
@@ -1,6 +1,7 @@
1extends "res://scripts/nodes/listeners/teleportListener.gd" 1extends "res://scripts/nodes/listeners/teleportListener.gd"
2 2
3var item_id 3var item_id
4var item_amount
4 5
5 6
6func _ready(): 7func _ready():
@@ -8,17 +9,27 @@ func _ready():
8 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() 9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
9 ) 10 )
10 11
11 print("node: %s" % node_path) 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
12 22
13 var gamedata = global.get_node("Gamedata") 23 var gamedata = global.get_node("Gamedata")
14 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 24 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
15 if door_id != null: 25 if door_id != null:
16 print("door_id: %d" % door_id)
17
18 var ap = global.get_node("Archipelago") 26 var ap = global.get_node("Archipelago")
19 item_id = ap.get_item_id_for_door(door_id) 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]
20 32
21 if item_id != null:
22 self.senders = [] 33 self.senders = []
23 self.senderGroup = [] 34 self.senderGroup = []
24 self.nested = false 35 self.nested = false
@@ -34,5 +45,5 @@ func _ready():
34func _readier(): 45func _readier():
35 var ap = global.get_node("Archipelago") 46 var ap = global.get_node("Archipelago")
36 47
37 if ap.has_item(item_id): 48 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered() 49 handleTriggered()
diff --git a/client/Archipelago/textclient.gd b/client/Archipelago/textclient.gd index 6a0aa95..85cc6d2 100644 --- a/client/Archipelago/textclient.gd +++ b/client/Archipelago/textclient.gd
@@ -50,7 +50,7 @@ func _ready():
50 50
51 51
52func _input(event): 52func _input(event):
53 if event is InputEventKey and event.pressed: 53 if global.loaded and event is InputEventKey and event.pressed:
54 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT): 54 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
55 if !get_tree().paused: 55 if !get_tree().paused:
56 is_open = true 56 is_open = true
@@ -76,10 +76,6 @@ func dismiss():
76 76
77 77
78func parse_printjson(text): 78func parse_printjson(text):
79 if !label.text.is_empty():
80 #label.newline()
81 pass
82
83 label.append_text("[p]" + text + "[/p]") 79 label.append_text("[p]" + text + "[/p]")
84 80
85 81
diff --git a/client/Archipelago/victoryListener.gd b/client/Archipelago/victoryListener.gd new file mode 100644 index 0000000..e9089d7 --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/visibilityListener.gd b/client/Archipelago/visibilityListener.gd new file mode 100644 index 0000000..5ea17a0 --- /dev/null +++ b/client/Archipelago/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/client/Archipelago/worldport.gd b/client/Archipelago/worldport.gd new file mode 100644 index 0000000..d0fb6c9 --- /dev/null +++ b/client/Archipelago/worldport.gd
@@ -0,0 +1,10 @@
1extends "res://scripts/nodes/worldport.gd"
2
3
4func _ready():
5 if global.map == "icarus" and exit == "daedalus":
6 var ap = global.get_node("Archipelago")
7 if not ap.daedalus_roof_access:
8 entry_point = Vector3(58, 10, 0)
9
10 super._ready()
diff --git a/client/Archipelago/worldportListener.gd b/client/Archipelago/worldportListener.gd index c31c825..5c2faff 100644 --- a/client/Archipelago/worldportListener.gd +++ b/client/Archipelago/worldportListener.gd
@@ -1,8 +1,8 @@
1extends "res://scripts/nodes/listeners/worldportListener.gd" 1extends "res://scripts/nodes/listeners/worldportListener.gd"
2 2
3 3
4func changeScene(): 4func handleTriggered():
5 if exit == "menus/credits": 5 if exit == "menus/credits":
6 return 6 return
7 7
8 super.changeScene() 8 super.handleTriggered()
diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md new file mode 100644 index 0000000..297b54d --- /dev/null +++ b/client/CHANGELOG.md
@@ -0,0 +1,38 @@
1# lingo2-archipelago Client Releases
2
3## v4.4 - 2025-09-13
4
5- Added support for anti-collectable trap items.
6- Fixed entrance to The Jubilant not opening properly when using control center
7 color shuffle.
8- Fixed the location "The Entry (Colored Doors Area) - OPEN" not sending.
9- Fixed level 2 letters not activating properly when letter shuffle is set to
10 Item Cyan.
11- Messages are now cleared out when returning to the main menu.
12- The player is prevented from accidentally breaking roof access logic when
13 returning to Daedalus from Icarus.
14
15Download:
16[lingo2-archipelago-client-v4.4.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v4.4.zip)<br/>
17Source:
18[v4.4](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v4.4)
19
20## v3.3 - 2025-09-12
21
22- Fixed issue downloading large datapackages (such as TUNIC's).
23- Connection failures now show error messages.
24
25Download:
26[lingo2-archipelago-client-v3.3.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v3.3.zip)<br/>
27Source:
28[v3.3](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v3.3)
29
30## v3.2 - 2025-09-12
31
32- Initial release for testing. Features include door shuffle, letter shuffle,
33 and symbol shuffle.
34
35Download:
36[lingo2-archipelago-client-v3.2.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v3.2.zip)<br/>
37Source:
38[v3.2](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v3.2)
diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..99589c5 --- /dev/null +++ b/client/README.md
@@ -0,0 +1,90 @@
1# Lingo 2 Archipelago Client
2
3The Lingo 2 Archipelago Client is a mod for Lingo 2 that allows you to connect
4to an Archipelago Multiworld and randomize your game.
5
6## Installation
7
81. Download the Lingo 2 Archipelago Randomizer from
9 [the releases page](https://code.fourisland.com/lingo2-archipelago/about/client/CHANGELOG.md).
102. Open up Lingo 2, go to settings, and click View Game Data. This should open
11 up a folder in Windows Explorer.
123. Unzip the randomizer into the "maps" folder. Ensure that archipelago.tscn and
13 the Archipelago folder are both within the maps folder.
14
15**NOTE**: It is important that the major version number of your client matches
16the major version number of the apworld you generated with.
17
18## Joining a Multiworld game
19
201. Launch Lingo 2.
212. Click on Level Selection, and choose Archipelago from the list.
223. The selected player is generally ignored by the mod, and you don't even need
23 to ensure you use the same player between connections. However, if your
24 player name has a gift map associated with it, Lingo 2 will prioritize the
25 gift map over loading the mod, so in that case you should choose another
26 player.
274. Press Play.
285. Enter the Archipelago address, slot name, and password into the fields.
296. Press Connect.
307. Enjoy!
31
32To continue an earlier game, you can perform the exact same steps as above. You
33will probably have to re-select Archipelago from the Level Selection screen, as
34the game does not remember which level you were playing.
35
36**Note**: Running the randomizer modifies the game's memory. If you want to play
37the base game after playing the randomizer, you need to restart Lingo 2 first.
38
39## Running from source
40
41The mod is mostly written in GDScript, which is parsed and executed by Lingo 2
42itself, and thus does not need to be compiled. However, there are two files that
43need to be generated before the client can be run.
44
45The first file is `data.binpb`, the datafile containing the randomizer logic.
46You can read about how to generate it on
47[its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md).
48Once you have it, put it in a subfolder of `client` called `generated`.
49
50The second generated file is `proto.gd`. This file allows Lingo 2 to read the
51datafile. We use a Godot script to generate it, which means
52[the Godot Editor](https://godotengine.org/download/) is required. From the root
53of the repository:
54
55```shell
56cd vendor\godobuf
57godot --headless -s addons\protobuf\protobuf_cmdln.gd --input=..\..\proto\data.proto ^
58 --output=..\..\client\Archipelago\generated\proto.gd
59```
60
61If you are not on Windows, replace the forward slashes with backslashes as
62appropriate (and the caret with a forward slash). You will also probably need to
63replace "godot" at the start of the second line with a path to a Godot Editor
64executable.
65
66After generating those two files, the contents of the `client` folder (minus
67this README) can be pasted into the Lingo 2 maps directory as described above.
68
69## Frequently Asked Questions
70
71### Is my progress saved locally?
72
73Lingo 2 autosaves your progress every time you solve a puzzle, get a
74collectable, or interact with a keyholder. The randomizer generates a savefile
75name based on your Multiworld seed and slot number, so you should be able to
76seamlessly switch between multiworlds and even slots within a multiworld.
77
78The exception to this is different rooms created from the same multiworld seed.
79The client is unable to tell rooms in a seed apart (this is a limitation of the
80Archipelago API), so the client will use the same save file for the same slot in
81different rooms on the same seed. You can work around this by manually moving or
82removing the save file from the level1 save file directory.
83
84If you play the base game again, you will see one or more save files with a long
85name that begins with "zzAP\_". These are the saves for your multiworlds. They
86can be safely deleted after you have completed the associated multiworld. It is
87not recommended to load these save files outside of the randomizer.
88
89A connection to Archipelago is required to resume playing a multiworld. This is
90because the set of items you have received is not stored locally.
diff --git a/client/archipelago.tscn b/client/archipelago.tscn index 40dd46f..da83b23 100644 --- a/client/archipelago.tscn +++ b/client/archipelago.tscn
@@ -40,6 +40,7 @@ offset_right = 1920.0
40offset_bottom = 225.0 40offset_bottom = 225.0
41text = "ARCHIPELAGO" 41text = "ARCHIPELAGO"
42valign = 1 42valign = 1
43horizontal_alignment = 1
43theme = ExtResource("2_g4bvn") 44theme = ExtResource("2_g4bvn")
44 45
45[node name="credit" parent="Panel" type="Label"] 46[node name="credit" parent="Panel" type="Label"]
@@ -150,6 +151,10 @@ caret_blink = true
150offset_right = 83.0 151offset_right = 83.0
151offset_bottom = 58.0 152offset_bottom = 58.0
152 153
154[node name="VersionMismatch" type="ConfirmationDialog" parent="Panel"]
155offset_right = 83.0
156offset_bottom = 58.0
157
153[node name="connection_history" type="MenuButton" parent="Panel"] 158[node name="connection_history" type="MenuButton" parent="Panel"]
154offset_left = 1239.0 159offset_left = 1239.0
155offset_top = 276.0 160offset_top = 276.0