diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/Archipelago/client.gd | 15 | ||||
-rw-r--r-- | client/Archipelago/collectable.gd | 16 | ||||
-rw-r--r-- | client/Archipelago/gamedata.gd | 4 | ||||
-rw-r--r-- | client/Archipelago/keyHolder.gd | 38 | ||||
-rw-r--r-- | client/Archipelago/keyHolderChecker.gd | 24 | ||||
-rw-r--r-- | client/Archipelago/keyHolderResetterListener.gd | 8 | ||||
-rw-r--r-- | client/Archipelago/keyboard.gd | 178 | ||||
-rw-r--r-- | client/Archipelago/manager.gd | 213 | ||||
-rw-r--r-- | client/Archipelago/pauseMenu.gd | 6 | ||||
-rw-r--r-- | client/Archipelago/player.gd | 17 | ||||
-rw-r--r-- | client/Archipelago/saver.gd | 6 | ||||
-rw-r--r-- | client/Archipelago/settings_screen.gd | 23 | ||||
-rw-r--r-- | client/Archipelago/textclient.gd | 2 | ||||
-rw-r--r-- | client/README.md | 97 |
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 | |||
41 | signal client_connected(slot_data) | 41 | signal client_connected(slot_data) |
42 | signal item_received(item_id, index, player, flags, amount) | 42 | signal item_received(item_id, index, player, flags, amount) |
43 | signal message_received(message) | 43 | signal message_received(message) |
44 | signal location_scout_received(item_id, location_id, player, flags) | ||
44 | 45 | ||
45 | 46 | ||
46 | func _init(): | 47 | func _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 | ||
406 | func scoutLocations(loc_ids): | ||
407 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) | ||
408 | |||
409 | |||
395 | func hasItem(item_id): | 410 | func 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 @@ | |||
1 | extends "res://scripts/nodes/collectable.gd" | ||
2 | |||
3 | |||
4 | func 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 | |||
15 | func 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 = {} | |||
8 | var door_id_by_ap_id = {} | 8 | var door_id_by_ap_id = {} |
9 | var map_id_by_name = {} | 9 | var map_id_by_name = {} |
10 | var progressive_id_by_ap_id = {} | 10 | var progressive_id_by_ap_id = {} |
11 | var letter_id_by_ap_id = {} | ||
11 | 12 | ||
12 | 13 | ||
13 | func _init(proto_script): | 14 | func _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 | ||
58 | func get_door_for_map_node_path(map_name, node_path): | 62 | func 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 @@ | |||
1 | extends "res://scripts/nodes/keyHolder.gd" | ||
2 | |||
3 | |||
4 | func 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 | |||
25 | func 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 | |||
33 | func 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 @@ | |||
1 | extends "res://scripts/nodes/listeners/keyHolderChecker.gd" | ||
2 | |||
3 | |||
4 | func 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 @@ | |||
1 | extends "res://scripts/nodes/listeners/keyHolderResetterListener.gd" | ||
2 | |||
3 | |||
4 | func 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 @@ | |||
1 | extends Node | ||
2 | |||
3 | const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz" | ||
4 | |||
5 | var letters_saved = {} | ||
6 | var letters_in_keyholders = [] | ||
7 | var letters_dynamic = {} | ||
8 | var keyholder_state = {} | ||
9 | |||
10 | var filename = "" | ||
11 | |||
12 | |||
13 | func _init(): | ||
14 | reset() | ||
15 | |||
16 | |||
17 | func reset(): | ||
18 | letters_saved.clear() | ||
19 | letters_in_keyholders.clear() | ||
20 | letters_dynamic.clear() | ||
21 | keyholder_state.clear() | ||
22 | |||
23 | |||
24 | func 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 | |||
62 | func 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 | |||
79 | func 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 | |||
102 | func 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 | |||
112 | func 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 | |||
122 | func 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 | |||
137 | func 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 | |||
151 | func 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 | |||
162 | func 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 | |||
3 | const my_version = "0.1.0" | 3 | const my_version = "0.1.0" |
4 | 4 | ||
5 | var SCRIPT_client | 5 | var SCRIPT_client |
6 | var SCRIPT_keyboard | ||
6 | var SCRIPT_locationListener | 7 | var SCRIPT_locationListener |
7 | var SCRIPT_uuid | 8 | var SCRIPT_uuid |
8 | var SCRIPT_victoryListener | 9 | var SCRIPT_victoryListener |
@@ -13,16 +14,39 @@ var ap_pass = "" | |||
13 | var connection_history = [] | 14 | var connection_history = [] |
14 | 15 | ||
15 | var client | 16 | var client |
17 | var keyboard | ||
16 | 18 | ||
17 | var _localdata_file = "" | 19 | var _localdata_file = "" |
18 | var _last_new_item = -1 | 20 | var _last_new_item = -1 |
19 | var _batch_locations = false | 21 | var _batch_locations = false |
20 | var _held_locations = [] | 22 | var _held_locations = [] |
23 | var _held_location_scouts = [] | ||
24 | var _location_scouts = {} | ||
21 | var _item_locks = {} | 25 | var _item_locks = {} |
26 | var _inverse_item_locks = {} | ||
27 | var _held_letters = {} | ||
28 | var _letters_setup = false | ||
22 | 29 | ||
30 | const kSHUFFLE_LETTERS_VANILLA = 0 | ||
31 | const kSHUFFLE_LETTERS_UNLOCKED = 1 | ||
32 | const kSHUFFLE_LETTERS_PROGRESSIVE = 2 | ||
33 | const kSHUFFLE_LETTERS_VANILLA_CYAN = 3 | ||
34 | const kSHUFFLE_LETTERS_ITEM_CYAN = 4 | ||
35 | |||
36 | const kLETTER_BEHAVIOR_VANILLA = 0 | ||
37 | const kLETTER_BEHAVIOR_ITEM = 1 | ||
38 | const kLETTER_BEHAVIOR_UNLOCKED = 2 | ||
39 | |||
40 | const kCYAN_DOOR_BEHAVIOR_H2 = 0 | ||
41 | const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1 | ||
42 | const kCYAN_DOOR_BEHAVIOR_ITEM = 2 | ||
43 | |||
44 | var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 | ||
23 | var daedalus_roof_access = false | 45 | var daedalus_roof_access = false |
24 | var keyholder_sanity = false | 46 | var keyholder_sanity = false |
47 | var shuffle_control_center_colors = false | ||
25 | var shuffle_doors = false | 48 | var shuffle_doors = false |
49 | var shuffle_letters = kSHUFFLE_LETTERS_VANILLA | ||
26 | var victory_condition = -1 | 50 | var victory_condition = -1 |
27 | 51 | ||
28 | signal could_not_connect | 52 | signal 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 | ||
70 | func saveSettings(): | 98 | func saveSettings(): |
71 | # Save the AP settings to disk. | 99 | # Save the AP settings to disk. |
@@ -100,6 +128,12 @@ func saveLocaldata(): | |||
100 | 128 | ||
101 | func connectToServer(): | 129 | func 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 | ||
301 | func _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 | |||
264 | func _client_could_not_connect(): | 324 | func _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 | ||
427 | func 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 | |||
328 | func stop_batching_locations(): | 439 | func 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 | ||
334 | func colorForItemType(flags): | 451 | func 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 | |||
466 | func 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 | |||
491 | func 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 | |||
502 | func _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" | |||
4 | func _pause_game(): | 4 | func _pause_game(): |
5 | global.get_node("Textclient").dismiss() | 5 | global.get_node("Textclient").dismiss() |
6 | super._pause_game() | 6 | super._pause_game() |
7 | |||
8 | |||
9 | func _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 | ||
4 | func levelLoaded(): | 4 | func 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 | ||
52 | func _input(event): | 52 | func _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 | |||
3 | The Lingo 2 Archipelago Client is a mod for Lingo 2 that allows you to connect | ||
4 | to an Archipelago Multiworld and randomize your game. | ||
5 | |||
6 | ## Installation | ||
7 | |||
8 | 1. Download the Lingo 2 Archipelago Randomizer from | ||
9 | [the releases page](https://code.fourisland.com/lingo2-archipelago/about/client/CHANGELOG.md). | ||
10 | 2. Open up Lingo 2, go to settings, and click View Game Data. This should open | ||
11 | up a folder in Windows Explorer. | ||
12 | 3. 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 | ||
16 | the major version number of the apworld you generated with. | ||
17 | |||
18 | ## Joining a Multiworld game | ||
19 | |||
20 | 1. Launch Lingo 2. | ||
21 | 2. Click on Level Selection, and choose Archipelago from the list. | ||
22 | 3. 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. | ||
27 | 4. Press Play. | ||
28 | 5. Enter the Archipelago address, slot name, and password into the fields. | ||
29 | 6. Press Connect. | ||
30 | 7. Enjoy! | ||
31 | |||
32 | To continue an earlier game, you can perform the exact same steps as above. You | ||
33 | will probably have to re-select Archipelago from the Level Selection screen, as | ||
34 | the 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 | ||
37 | the base game after playing the randomizer, you need to restart Lingo 2 first. | ||
38 | |||
39 | ## Running from source | ||
40 | |||
41 | The mod is mostly written in GDScript, which is parsed and executed by Lingo 2 | ||
42 | itself, and thus does not need to be compiled. However, there are two files that | ||
43 | need to be generated before the client can be run. | ||
44 | |||
45 | The first file is `data.binpb`, the datafile containing the randomizer logic. | ||
46 | You can read about how to generate it on | ||
47 | [its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md). | ||
48 | Once you have it, put it in a subfolder of `client` called `generated`. | ||
49 | |||
50 | The second generated file is `proto.gd`. This file allows Lingo 2 to read the | ||
51 | datafile. We use a Godot script to generate it, which means | ||
52 | [the Godot Editor](https://godotengine.org/download/) is required. From the root | ||
53 | of the repository: | ||
54 | |||
55 | ```shell | ||
56 | cd vendor\godobuf | ||
57 | godot --headless -s addons\protobuf\protobuf_cmdln.gd --input=..\..\proto\data.proto ^ | ||
58 | --output=..\..\client\Archipelago\generated\proto.gd | ||
59 | ``` | ||
60 | |||
61 | If you are not on Windows, replace the forward slashes with backslashes as | ||
62 | appropriate (and the caret with a forward slash). You will also probably need to | ||
63 | replace "godot" at the start of the second line with a path to a Godot Editor | ||
64 | executable. | ||
65 | |||
66 | After generating those two files, the contents of the `client` folder (minus | ||
67 | this 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 | |||
73 | Lingo 2 autosaves your progress every time you solve a puzzle, get a | ||
74 | collectable, or interact with a keyholder. The randomizer generates a savefile | ||
75 | name based on your Multiworld seed and slot number, so you should be able to | ||
76 | seamlessly switch between multiworlds and even slots within a multiworld. | ||
77 | |||
78 | The exception to this is different rooms created from the same multiworld seed. | ||
79 | The client is unable to tell rooms in a seed apart (this is a limitation of the | ||
80 | Archipelago API), so the client will use the same save file for the same slot in | ||
81 | different rooms on the same seed. You can work around this by manually moving or | ||
82 | removing the save file from the level1 save file directory. | ||
83 | |||
84 | If you play the base game again, you will see one or more save files with a long | ||
85 | name that begins with "zzAP\_". These are the saves for your multiworlds. They | ||
86 | can be safely deleted after you have completed the associated multiworld. It is | ||
87 | not recommended to load these save files outside of the randomizer. | ||
88 | |||
89 | A connection to Archipelago is required to resume playing a multiworld. This is | ||
90 | because 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 | ||
95 | other side of opaque walls. The player is never expected to or required to do | ||
96 | this in normal gameplay. This randomizer does not change how wall snipes work, | ||
97 | but it will likewise never require the use of them. | ||