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/client.gd15
-rw-r--r--client/Archipelago/collectable.gd16
-rw-r--r--client/Archipelago/gamedata.gd4
-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.gd178
-rw-r--r--client/Archipelago/manager.gd213
-rw-r--r--client/Archipelago/pauseMenu.gd6
-rw-r--r--client/Archipelago/player.gd17
-rw-r--r--client/Archipelago/saver.gd6
-rw-r--r--client/Archipelago/settings_screen.gd23
-rw-r--r--client/Archipelago/textclient.gd2
-rw-r--r--client/README.md97
14 files changed, 610 insertions, 37 deletions
diff --git a/client/Archipelago/client.gd b/client/Archipelago/client.gd index 428560e..2e080fd 100644 --- a/client/Archipelago/client.gd +++ b/client/Archipelago/client.gd
@@ -41,6 +41,7 @@ signal connect_status
41signal client_connected(slot_data) 41signal client_connected(slot_data)
42signal item_received(item_id, index, player, flags, amount) 42signal item_received(item_id, index, player, flags, amount)
43signal message_received(message) 43signal message_received(message)
44signal location_scout_received(item_id, location_id, player, flags)
44 45
45 46
46func _init(): 47func _init():
@@ -257,6 +258,16 @@ func _process(_delta):
257 elif cmd == "PrintJSON": 258 elif cmd == "PrintJSON":
258 emit_signal("message_received", message) 259 emit_signal("message_received", message)
259 260
261 elif cmd == "LocationInfo":
262 for loc in message["locations"]:
263 emit_signal(
264 "location_scout_received",
265 int(loc["item"]),
266 int(loc["location"]),
267 int(loc["player"]),
268 int(loc["flags"])
269 )
270
260 elif state == WebSocketPeer.STATE_CLOSED: 271 elif state == WebSocketPeer.STATE_CLOSED:
261 if _has_connected: 272 if _has_connected:
262 _closed() 273 _closed()
@@ -392,6 +403,10 @@ func completedGoal():
392 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL 403 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
393 404
394 405
406func scoutLocations(loc_ids):
407 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
408
409
395func hasItem(item_id): 410func hasItem(item_id):
396 return _received_items.has(item_id) 411 return _received_items.has(item_id)
397 412
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/gamedata.gd b/client/Archipelago/gamedata.gd index 669ad3d..f7a5d90 100644 --- a/client/Archipelago/gamedata.gd +++ b/client/Archipelago/gamedata.gd
@@ -8,6 +8,7 @@ var painting_id_by_map_node_path = {}
8var door_id_by_ap_id = {} 8var door_id_by_ap_id = {}
9var map_id_by_name = {} 9var map_id_by_name = {}
10var progressive_id_by_ap_id = {} 10var progressive_id_by_ap_id = {}
11var letter_id_by_ap_id = {}
11 12
12 13
13func _init(proto_script): 14func _init(proto_script):
@@ -54,6 +55,9 @@ func load(data_bytes):
54 for progressive in objects.get_progressives(): 55 for progressive in objects.get_progressives():
55 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id() 56 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
56 57
58 for letter in objects.get_letters():
59 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
60
57 61
58func get_door_for_map_node_path(map_name, node_path): 62func get_door_for_map_node_path(map_name, node_path):
59 if not door_id_by_map_node_path.has(map_name): 63 if not door_id_by_map_node_path.has(map_name):
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..600a047 --- /dev/null +++ b/client/Archipelago/keyboard.gd
@@ -0,0 +1,178 @@
1extends Node
2
3const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz"
4
5var letters_saved = {}
6var letters_in_keyholders = []
7var letters_dynamic = {}
8var keyholder_state = {}
9
10var filename = ""
11
12
13func _init():
14 reset()
15
16
17func reset():
18 letters_saved.clear()
19 letters_in_keyholders.clear()
20 letters_dynamic.clear()
21 keyholder_state.clear()
22
23
24func load_seed():
25 var ap = global.get_node("Archipelago")
26
27 reset()
28
29 filename = "user://archipelago_keys/%s_%d" % [ap.client._seed, ap.client._slot]
30
31 if FileAccess.file_exists(filename):
32 var ap_file = FileAccess.open(filename, FileAccess.READ)
33 var localdata = []
34 if ap_file != null:
35 localdata = ap_file.get_var(true)
36 ap_file.close()
37
38 if typeof(localdata) != TYPE_ARRAY:
39 print("AP keyboard file is corrupted")
40 localdata = []
41
42 if localdata.size() > 0:
43 letters_saved = localdata[0]
44 if localdata.size() > 1:
45 letters_in_keyholders = localdata[1]
46 if localdata.size() > 2:
47 keyholder_state = localdata[2]
48
49 for k in kALL_LETTERS:
50 var level = 0
51
52 if ap.get_letter_behavior(k, false) == ap.kLETTER_BEHAVIOR_UNLOCKED:
53 level += 1
54 if ap.get_letter_behavior(k, true) == ap.kLETTER_BEHAVIOR_UNLOCKED:
55 level += 1
56
57 letters_dynamic[k] = level
58
59 update_unlocks()
60
61
62func save():
63 var dir = DirAccess.open("user://")
64 var folder = "archipelago_keys"
65 if not dir.dir_exists(folder):
66 dir.make_dir(folder)
67
68 var file = FileAccess.open(filename, FileAccess.WRITE)
69
70 var data = [
71 letters_saved,
72 letters_in_keyholders,
73 keyholder_state,
74 ]
75 file.store_var(data, true)
76 file.close()
77
78
79func update_unlocks():
80 unlocks.resetKeys()
81
82 var has_doubles = false
83
84 for k in kALL_LETTERS:
85 var level = 0
86
87 if not letters_in_keyholders.has(k):
88 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0)
89
90 if level >= 2:
91 level = 2
92 has_doubles = true
93
94 unlocks.unlockKey(k, level)
95
96 if has_doubles and unlocks.data["double_letters"] != "unlocked":
97 var ap = global.get_node("Archipelago")
98 if ap.cyan_door_behavior == ap.kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER:
99 unlocks.setData("double_letters", "unlocked")
100
101
102func collect_local_letter(key, level):
103 if level < 0 or level > 2 or level < letters_saved.get(key, 0):
104 return
105
106 letters_saved[key] = level
107
108 update_unlocks()
109 save()
110
111
112func collect_remote_letter(key, level):
113 if level < 0 or level > 2 or level < letters_dynamic.get(key, 0):
114 return
115
116 letters_dynamic[key] = level
117
118 update_unlocks()
119 save()
120
121
122func put_in_keyholder(key, map, kh_path):
123 if not keyholder_state.has(map):
124 keyholder_state[map] = {}
125
126 keyholder_state[map][kh_path] = key
127 letters_in_keyholders.append(key)
128
129 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(
130 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
131 )
132
133 update_unlocks()
134 save()
135
136
137func remove_from_keyholder(key, map, kh_path):
138 if not keyholder_state.has(map):
139 # This... shouldn't happen.
140 keyholder_state[map] = {}
141
142 keyholder_state[map].erase(kh_path)
143 letters_in_keyholders.erase(key)
144
145 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(key, 0)
146
147 update_unlocks()
148 save()
149
150
151func load_keyholders(map):
152 if keyholder_state.has(map):
153 var khs = keyholder_state[map]
154
155 for path in khs.keys():
156 var key = khs[path]
157 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
158 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
159 )
160
161
162func reset_keyholders():
163 if letters_in_keyholders.is_empty():
164 return false
165
166 if keyholder_state.has(global.map):
167 for path in keyholder_state[global.map]:
168 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
169 keyholder_state[global.map][path], 0
170 )
171
172 keyholder_state.clear()
173 letters_in_keyholders.clear()
174
175 update_unlocks()
176 save()
177
178 return true
diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd index 609e645..cd0654f 100644 --- a/client/Archipelago/manager.gd +++ b/client/Archipelago/manager.gd
@@ -3,6 +3,7 @@ extends Node
3const my_version = "0.1.0" 3const my_version = "0.1.0"
4 4
5var SCRIPT_client 5var SCRIPT_client
6var SCRIPT_keyboard
6var SCRIPT_locationListener 7var SCRIPT_locationListener
7var SCRIPT_uuid 8var SCRIPT_uuid
8var SCRIPT_victoryListener 9var SCRIPT_victoryListener
@@ -13,16 +14,39 @@ var ap_pass = ""
13var connection_history = [] 14var connection_history = []
14 15
15var client 16var client
17var keyboard
16 18
17var _localdata_file = "" 19var _localdata_file = ""
18var _last_new_item = -1 20var _last_new_item = -1
19var _batch_locations = false 21var _batch_locations = false
20var _held_locations = [] 22var _held_locations = []
23var _held_location_scouts = []
24var _location_scouts = {}
21var _item_locks = {} 25var _item_locks = {}
26var _inverse_item_locks = {}
27var _held_letters = {}
28var _letters_setup = false
22 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 cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
23var daedalus_roof_access = false 45var daedalus_roof_access = false
24var keyholder_sanity = false 46var keyholder_sanity = false
47var shuffle_control_center_colors = false
25var shuffle_doors = false 48var shuffle_doors = false
49var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
26var victory_condition = -1 50var victory_condition = -1
27 51
28signal could_not_connect 52signal could_not_connect
@@ -60,12 +84,16 @@ func _ready():
60 84
61 client.connect("item_received", _process_item) 85 client.connect("item_received", _process_item)
62 client.connect("message_received", _process_message) 86 client.connect("message_received", _process_message)
87 client.connect("location_scout_received", _process_location_scout)
63 client.connect("could_not_connect", _client_could_not_connect) 88 client.connect("could_not_connect", _client_could_not_connect)
64 client.connect("connect_status", _client_connect_status) 89 client.connect("connect_status", _client_connect_status)
65 client.connect("client_connected", _client_connected) 90 client.connect("client_connected", _client_connected)
66 91
67 add_child(client) 92 add_child(client)
68 93
94 keyboard = SCRIPT_keyboard.new()
95 add_child(keyboard)
96
69 97
70func saveSettings(): 98func saveSettings():
71 # Save the AP settings to disk. 99 # Save the AP settings to disk.
@@ -100,6 +128,12 @@ func saveLocaldata():
100 128
101func connectToServer(): 129func connectToServer():
102 _last_new_item = -1 130 _last_new_item = -1
131 _batch_locations = false
132 _held_locations = []
133 _held_location_scouts = []
134 _location_scouts = {}
135 _letters_setup = false
136 _held_letters = {}
103 137
104 client.connectToServer(ap_server, ap_user, ap_pass) 138 client.connectToServer(ap_server, ap_user, ap_pass)
105 139
@@ -122,29 +156,32 @@ func _process_item(item, index, from, flags, amount):
122 item_name = client._item_id_to_name["Lingo 2"][item] 156 item_name = client._item_id_to_name["Lingo 2"][item]
123 157
124 var gamedata = global.get_node("Gamedata") 158 var gamedata = global.get_node("Gamedata")
125 var door_id = gamedata.door_id_by_ap_id.get(item, null)
126 var prog_id = null
127 159
128 if door_id == null: 160 var prog_id = null
129 prog_id = gamedata.progressive_id_by_ap_id.get(item, null) 161 if _inverse_item_locks.has(item):
130 if prog_id != null: 162 for lock in _inverse_item_locks.get(item):
131 var progressive = gamedata.objects.get_progressives()[prog_id] 163 if lock[1] != amount:
132 if progressive.get_doors().size() >= amount: 164 continue
133 door_id = progressive.get_doors()[amount - 1] 165
134 166 if gamedata.progressive_id_by_ap_id.has(item):
135 if door_id != null and gamedata.get_door_map_name(door_id) == global.map: 167 prog_id = lock[0]
136 var receivers = gamedata.get_door_receivers(door_id) 168
137 var scene = get_tree().get_root().get_node_or_null("scene") 169 if gamedata.get_door_map_name(lock[0]) != global.map:
138 if scene != null: 170 continue
139 for receiver in receivers: 171
140 var rnode = scene.get_node_or_null(receiver) 172 var receivers = gamedata.get_door_receivers(lock[0])
141 if rnode != null: 173 var scene = get_tree().get_root().get_node_or_null("scene")
142 rnode.handleTriggered() 174 if scene != null:
143 #for painting_id in gamedata.objects.get_doors()[door_id].get_move_paintings(): 175 for receiver in receivers:
144 # var painting = gamedata.objects.get_paintings()[painting_id] 176 var rnode = scene.get_node_or_null(receiver)
145 # var pnode = scene.get_node_or_null(painting.get_path() + "/teleportListener") 177 if rnode != null:
146 # if pnode != null: 178 rnode.handleTriggered()
147 # pnode.handleTriggered() 179
180 var letter_id = gamedata.letter_id_by_ap_id.get(item, null)
181 if letter_id != null:
182 var letter = gamedata.objects.get_letters()[letter_id]
183 if not letter.has_level2() or not letter.get_level2():
184 _process_key_item(letter.get_key(), amount)
148 185
149 # Show a message about the item if it's new. 186 # Show a message about the item if it's new.
150 if index != null and index > _last_new_item: 187 if index != null and index > _last_new_item:
@@ -158,8 +195,8 @@ func _process_item(item, index, from, flags, amount):
158 var item_color = colorForItemType(flags) 195 var item_color = colorForItemType(flags)
159 196
160 var full_item_name = item_name 197 var full_item_name = item_name
161 if prog_id != null and door_id != null: 198 if prog_id != null:
162 var door = gamedata.objects.get_doors()[door_id] 199 var door = gamedata.objects.get_doors()[prog_id]
163 full_item_name = "%s (%s)" % [item_name, door.get_name()] 200 full_item_name = "%s (%s)" % [item_name, door.get_name()]
164 201
165 var message 202 var message
@@ -261,6 +298,29 @@ func parse_printjson_for_textclient(message):
261 textclient_node.parse_printjson("".join(parts)) 298 textclient_node.parse_printjson("".join(parts))
262 299
263 300
301func _process_location_scout(item_id, location_id, player, flags):
302 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags}
303
304 var gamedata = global.get_node("Gamedata")
305 var map_id = gamedata.map_id_by_name.get(global.map)
306
307 var item_name = "Unknown"
308 var item_player_game = client._game_by_player[float(player)]
309 if client._item_id_to_name[item_player_game].has(item_id):
310 item_name = client._item_id_to_name[item_player_game][item_id]
311
312 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
313 if letter_id != null:
314 var letter = gamedata.objects.get_letters()[letter_id]
315 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
316 if room.get_map_id() == map_id:
317 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
318 letter.get_path()
319 )
320 if collectable != null:
321 collectable.setScoutedText(item_name)
322
323
264func _client_could_not_connect(): 324func _client_could_not_connect():
265 emit_signal("could_not_connect") 325 emit_signal("could_not_connect")
266 326
@@ -290,9 +350,12 @@ func _client_connected(slot_data):
290 _last_new_item = localdata[0] 350 _last_new_item = localdata[0]
291 351
292 # Read slot data. 352 # Read slot data.
353 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
293 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) 354 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
294 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) 355 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
356 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
295 shuffle_doors = bool(slot_data.get("shuffle_doors", false)) 357 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
358 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
296 victory_condition = int(slot_data.get("victory_condition", 0)) 359 victory_condition = int(slot_data.get("victory_condition", 0))
297 360
298 # Set up item locks. 361 # Set up item locks.
@@ -311,6 +374,42 @@ func _client_connected(slot_data):
311 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]] 374 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
312 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1] 375 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
313 376
377 for door_group in gamedata.objects.get_door_groups():
378 if (
379 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR
380 or door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP
381 ):
382 for door in door_group.get_doors():
383 _item_locks[door] = [door_group.get_ap_id(), 1]
384
385 if shuffle_control_center_colors:
386 for door in gamedata.objects.get_doors():
387 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
388 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
389
390 for door_group in gamedata.objects.get_door_groups():
391 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR:
392 for door in door_group.get_doors():
393 _item_locks[door] = [door_group.get_ap_id(), 1]
394
395 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
396 for door_group in gamedata.objects.get_door_groups():
397 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
398 for door in door_group.get_doors():
399 if not _item_locks.has(door):
400 _item_locks[door] = [door_group.get_ap_id(), 1]
401
402 # Create a reverse item locks map for processing items.
403 _inverse_item_locks = {}
404
405 for door_id in _item_locks.keys():
406 var lock = _item_locks.get(door_id)
407
408 if not _inverse_item_locks.has(lock[0]):
409 _inverse_item_locks[lock[0]] = []
410
411 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
412
314 emit_signal("ap_connected") 413 emit_signal("ap_connected")
315 414
316 415
@@ -325,10 +424,28 @@ func send_location(loc_id):
325 client.sendLocation(loc_id) 424 client.sendLocation(loc_id)
326 425
327 426
427func scout_location(loc_id):
428 if _location_scouts.has(loc_id):
429 return _location_scouts.get(loc_id)
430
431 if _batch_locations:
432 _held_location_scouts.append(loc_id)
433 else:
434 client.scoutLocation(loc_id)
435
436 return null
437
438
328func stop_batching_locations(): 439func stop_batching_locations():
329 _batch_locations = false 440 _batch_locations = false
330 client.sendLocations(_held_locations) 441
331 _held_locations.clear() 442 if not _held_locations.is_empty():
443 client.sendLocations(_held_locations)
444 _held_locations.clear()
445
446 if not _held_location_scouts.is_empty():
447 client.scoutLocations(_held_location_scouts)
448 _held_location_scouts.clear()
332 449
333 450
334func colorForItemType(flags): 451func colorForItemType(flags):
@@ -344,3 +461,47 @@ func colorForItemType(flags):
344 return "#d63a22" 461 return "#d63a22"
345 else: # filler 462 else: # filler
346 return "#14de9e" 463 return "#14de9e"
464
465
466func get_letter_behavior(key, level2):
467 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
468 return kLETTER_BEHAVIOR_UNLOCKED
469
470 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
471 if level2:
472 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
473 return kLETTER_BEHAVIOR_VANILLA
474 else:
475 return kLETTER_BEHAVIOR_ITEM
476 else:
477 return kLETTER_BEHAVIOR_UNLOCKED
478
479 if not level2 and ["h", "i", "n", "t"].has(key):
480 # This differs from the equivalent function in the apworld. Logically it is
481 # the same as UNLOCKED since they are in the starting room, but VANILLA
482 # means the player still has to actually pick up the letters.
483 return kLETTER_BEHAVIOR_VANILLA
484
485 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
486 return kLETTER_BEHAVIOR_ITEM
487
488 return kLETTER_BEHAVIOR_VANILLA
489
490
491func setup_keys():
492 keyboard.load_seed()
493
494 _letters_setup = true
495
496 for k in _held_letters.keys():
497 _process_key_item(k, _held_letters[k])
498
499 _held_letters.clear()
500
501
502func _process_key_item(key, level):
503 if not _letters_setup:
504 _held_letters[key] = max(_held_letters.get(key, 0), level)
505 return
506
507 keyboard.collect_remote_letter(key, level)
diff --git a/client/Archipelago/pauseMenu.gd b/client/Archipelago/pauseMenu.gd index 6c013a5..5da114a 100644 --- a/client/Archipelago/pauseMenu.gd +++ b/client/Archipelago/pauseMenu.gd
@@ -4,3 +4,9 @@ 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 super._main_menu()
diff --git a/client/Archipelago/player.gd b/client/Archipelago/player.gd index 4569af5..dd6aa2b 100644 --- a/client/Archipelago/player.gd +++ b/client/Archipelago/player.gd
@@ -81,6 +81,23 @@ func _ready():
81 81
82 get_parent().add_child.call_deferred(locationListener) 82 get_parent().add_child.call_deferred(locationListener)
83 83
84 if (
85 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
86 != ap.kLETTER_BEHAVIOR_VANILLA
87 ):
88 var scout = ap.scout_location(letter.get_ap_id())
89 if scout != null:
90 var item_name = "Unknown"
91 var item_player_game = ap.client._game_by_player[float(scout["player"])]
92 if ap.client._item_id_to_name[item_player_game].has(scout["item"]):
93 item_name = ap.client._item_id_to_name[item_player_game][scout["item"]]
94
95 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
96 letter.get_path()
97 )
98 if collectable != null:
99 collectable.setScoutedText.call_deferred(item_name)
100
84 # Set up mastery locations. 101 # Set up mastery locations.
85 for mastery in gamedata.objects.get_masteries(): 102 for mastery in gamedata.objects.get_masteries():
86 var room = gamedata.objects.get_rooms()[mastery.get_room_id()] 103 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
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 ed9571d..aaaf72a 100644 --- a/client/Archipelago/settings_screen.gd +++ b/client/Archipelago/settings_screen.gd
@@ -22,14 +22,8 @@ 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")
35 ap_instance.SCRIPT_victoryListener = load("user://maps/Archipelago/victoryListener.gd") 29 ap_instance.SCRIPT_victoryListener = load("user://maps/Archipelago/victoryListener.gd")
@@ -38,7 +32,13 @@ func _ready():
38 32
39 # Let's also inject any scripts we need to inject now. 33 # Let's also inject any scripts we need to inject now.
40 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"))
41 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 )
42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd")) 42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd")) 43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd"))
44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd")) 44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
@@ -141,17 +141,22 @@ func connectionSuccessful():
141 global.universe = "lingo" 141 global.universe = "lingo"
142 global.map = "the_entry" 142 global.map = "the_entry"
143 143
144 unlocks.resetKeys()
145 unlocks.resetCollectables() 144 unlocks.resetCollectables()
146 unlocks.resetData() 145 unlocks.resetData()
147 unlocks.loadKeys() 146
147 ap.setup_keys()
148
148 unlocks.loadCollectables() 149 unlocks.loadCollectables()
149 unlocks.loadData() 150 unlocks.loadData()
150 unlocks.unlockKey("capslock", 1) 151 unlocks.unlockKey("capslock", 1)
151 152
152 clearResourceCache("res://objects/meshes/gridDoor.tscn") 153 clearResourceCache("res://objects/meshes/gridDoor.tscn")
154 clearResourceCache("res://objects/nodes/collectable.tscn")
153 clearResourceCache("res://objects/nodes/door.tscn") 155 clearResourceCache("res://objects/nodes/door.tscn")
156 clearResourceCache("res://objects/nodes/keyHolder.tscn")
154 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn") 157 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn")
158 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
159 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
155 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") 160 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
156 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") 161 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
157 clearResourceCache("res://objects/nodes/player.tscn") 162 clearResourceCache("res://objects/nodes/player.tscn")
diff --git a/client/Archipelago/textclient.gd b/client/Archipelago/textclient.gd index 4b03151..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
diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..1e94bdb --- /dev/null +++ b/client/README.md
@@ -0,0 +1,97 @@
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.
91
92### What about wall snipes?
93
94"Wall sniping" refers to the fact that you are able to solve puzzles on the
95other side of opaque walls. The player is never expected to or required to do
96this in normal gameplay. This randomizer does not change how wall snipes work,
97but it will likewise never require the use of them.