about summary refs log tree commit diff stats
path: root/apworld/client/manager.gd
diff options
context:
space:
mode:
Diffstat (limited to 'apworld/client/manager.gd')
-rw-r--r--apworld/client/manager.gd778
1 files changed, 778 insertions, 0 deletions
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd new file mode 100644 index 0000000..9b790c0 --- /dev/null +++ b/apworld/client/manager.gd
@@ -0,0 +1,778 @@
1extends Node
2
3var SCRIPT_client
4var SCRIPT_keyboard
5var SCRIPT_locationListener
6var SCRIPT_minimap
7var SCRIPT_victoryListener
8var SCRIPT_websocketserver
9
10var ap_server = ""
11var ap_user = ""
12var ap_pass = ""
13var connection_history = []
14var show_compass = false
15var show_locations = false
16var show_minimap = false
17var prioritize_current_map = false
18
19var client
20var keyboard
21
22var _localdata_file = ""
23var _last_new_item = -1
24var _batch_locations = false
25var _held_locations = []
26var _held_location_scouts = []
27var _location_scouts = {}
28var _item_locks = {}
29var _inverse_item_locks = {}
30var _held_letters = {}
31var _letters_setup = false
32var _already_connected = false
33var _ignored_locations = []
34var _map_scripts = {}
35
36const kSHUFFLE_LETTERS_VANILLA = 0
37const kSHUFFLE_LETTERS_UNLOCKED = 1
38const kSHUFFLE_LETTERS_PROGRESSIVE = 2
39const kSHUFFLE_LETTERS_VANILLA_CYAN = 3
40const kSHUFFLE_LETTERS_ITEM_CYAN = 4
41
42const kLETTER_BEHAVIOR_VANILLA = 0
43const kLETTER_BEHAVIOR_ITEM = 1
44const kLETTER_BEHAVIOR_UNLOCKED = 2
45
46const kCYAN_DOOR_BEHAVIOR_H2 = 0
47const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
48const kCYAN_DOOR_BEHAVIOR_ITEM = 2
49
50const kFAST_TRAVEL_ACCESS_VANILLA = 0
51const kFAST_TRAVEL_ACCESS_UNLOCKED = 1
52const kFAST_TRAVEL_ACCESS_ITEMS = 2
53
54const kEndingNameByVictoryValue = {
55 0: "GRAY",
56 1: "PURPLE",
57 2: "MINT",
58 3: "BLACK",
59 4: "BLUE",
60 5: "CYAN",
61 6: "RED",
62 7: "PLUM",
63 8: "ORANGE",
64 9: "GOLD",
65 10: "YELLOW",
66 11: "GREEN",
67 12: "WHITE",
68}
69
70var apworld_version = [0, 0, 0]
71var custom_mint_ending = ""
72var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
73var daedalus_only = false
74var daedalus_roof_access = false
75var enable_gift_maps = []
76var enable_icarus = false
77var endings_requirement = 0
78var fast_travel_access = 0
79var keyholder_sanity = false
80var masteries_requirement = 0
81var music_mapping = {}
82var port_pairings = {}
83var rte_mapping = []
84var shuffle_control_center_colors = false
85var shuffle_doors = false
86var shuffle_gallery_paintings = false
87var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
88var shuffle_symbols = false
89var shuffle_worldports = false
90var slot_rng = null
91var strict_cyan_ending = false
92var strict_purple_ending = false
93var victory_condition = -1
94
95var color_by_material_path = {}
96
97signal could_not_connect
98signal connect_status
99signal ap_connected
100
101
102func _init():
103 # Read AP settings from file, if there are any
104 if FileAccess.file_exists("user://ap_settings"):
105 var file = FileAccess.open("user://ap_settings", FileAccess.READ)
106 var data = file.get_var(true)
107 file.close()
108
109 if typeof(data) != TYPE_ARRAY:
110 global._print("AP settings file is corrupted")
111 data = []
112
113 if data.size() > 0:
114 ap_server = data[0]
115
116 if data.size() > 1:
117 ap_user = data[1]
118
119 if data.size() > 2:
120 ap_pass = data[2]
121
122 if data.size() > 3:
123 connection_history = data[3]
124
125 if data.size() > 4:
126 show_compass = data[4]
127
128 if data.size() > 5:
129 show_locations = data[5]
130
131 if data.size() > 6:
132 show_minimap = data[6]
133
134 if data.size() > 7:
135 prioritize_current_map = data[7]
136
137 # We need to create a mapping from material paths to the original colors of
138 # those materials. We force reload the materials, overwriting any custom
139 # textures, and create the mapping. We then reload the textures in case the
140 # player had a custom one enabled.
141 var directory = DirAccess.open("res://assets/materials")
142 for material_name in directory.get_files():
143 var material = ResourceLoader.load(
144 "res://assets/materials/" + material_name, "", ResourceLoader.CACHE_MODE_REPLACE
145 )
146
147 color_by_material_path[material.resource_path] = Color(material.albedo_color)
148
149 settings.load_user_textures()
150
151
152func _ready():
153 client = SCRIPT_client.new()
154 client.SCRIPT_websocketserver = SCRIPT_websocketserver
155
156 client.item_received.connect(_process_item)
157 client.location_scout_received.connect(_process_location_scout)
158 client.text_message_received.connect(_process_text_message)
159 client.item_sent_notification.connect(_process_item_sent_notification)
160 client.hint_received.connect(_process_hint_received)
161 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
162 client.checked_locations_updated.connect(_on_checked_locations_updated)
163 client.ignored_locations_updated.connect(_on_ignored_locations_updated)
164 client.hinted_locations_updated.connect(_on_hinted_locations_updated)
165 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
166 client.door_latched.connect(_on_door_latched)
167
168 client.could_not_connect.connect(_client_could_not_connect)
169 client.connect_status.connect(_client_connect_status)
170 client.client_connected.connect(_client_connected)
171
172 add_child(client)
173
174 keyboard = SCRIPT_keyboard.new()
175 add_child(keyboard)
176 client.keyboard_update_received.connect(keyboard.remote_keyboard_updated)
177
178
179func saveSettings():
180 # Save the AP settings to disk.
181 var path = "user://ap_settings"
182 var file = FileAccess.open(path, FileAccess.WRITE)
183
184 var data = [
185 ap_server,
186 ap_user,
187 ap_pass,
188 connection_history,
189 show_compass,
190 show_locations,
191 show_minimap,
192 prioritize_current_map,
193 ]
194 file.store_var(data, true)
195 file.close()
196
197
198func saveLocaldata():
199 # Save the MW/slot specific settings to disk.
200 var dir = DirAccess.open("user://")
201 var folder = "archipelago_data"
202 if not dir.dir_exists(folder):
203 dir.make_dir(folder)
204
205 var file = FileAccess.open(_localdata_file, FileAccess.WRITE)
206
207 var data = [
208 _last_new_item,
209 ]
210 file.store_var(data, true)
211 file.close()
212
213
214func connectToServer():
215 _last_new_item = -1
216 _batch_locations = false
217 _held_locations = []
218 _held_location_scouts = []
219 _location_scouts = {}
220 _letters_setup = false
221 _held_letters = {}
222 _already_connected = false
223
224 client.connectToServer(ap_server, ap_user, ap_pass)
225
226
227func getSaveFileName():
228 return "zzAP_%s_%d" % [client._seed, client._slot]
229
230
231func disconnect_from_ap():
232 _already_connected = false
233
234 var effects = global.get_node("Effects")
235 effects.set_connection_lost(false)
236
237 client.disconnect_from_ap()
238
239
240func get_item_id_for_door(door_id):
241 return _item_locks.get(door_id, null)
242
243
244func _process_item(item, amount):
245 var gamedata = global.get_node("Gamedata")
246
247 var item_id = int(item["id"])
248 var prog_id = null
249 if _inverse_item_locks.has(item_id):
250 for lock in _inverse_item_locks.get(item_id):
251 if lock[1] != amount:
252 continue
253
254 if gamedata.progressive_id_by_ap_id.has(item_id):
255 prog_id = lock[0]
256
257 if gamedata.get_door_map_name(lock[0]) != global.map:
258 continue
259
260 # TODO: fix doors opening from door groups
261 var receivers = gamedata.get_door_receivers(lock[0])
262 var scene = get_tree().get_root().get_node_or_null("scene")
263 if scene != null:
264 for receiver in receivers:
265 var rnode = scene.get_node_or_null(receiver)
266 if rnode != null:
267 rnode.handleTriggered()
268
269 var letter_id = gamedata.letter_id_by_ap_id.get(item_id, null)
270 if letter_id != null:
271 var letter = gamedata.objects.get_letters()[letter_id]
272 if not letter.has_level2() or not letter.get_level2():
273 _process_key_item(letter.get_key(), amount)
274
275 if gamedata.symbol_item_ids.has(item_id):
276 var player = get_tree().get_root().get_node_or_null("scene/player")
277 if player != null:
278 player.evaluate_solvability.emit()
279
280 if item_id == gamedata.objects.get_special_ids()["A Job Well Done"]:
281 update_job_well_done_sign()
282
283 if item_id == gamedata.objects.get_special_ids()["Numbers"] and global.map == "the_fuzzy":
284 global.allow_numbers = true
285
286 if gamedata.map_id_by_rte_ap_id.has(item_id):
287 var rteInner = get_tree().get_root().get_node_or_null(
288 "scene/player/pause_menu/menu/return/rteInner"
289 )
290 if rteInner != null:
291 rteInner.refreshButtons()
292
293 # Show a message about the item if it's new.
294 if int(item["index"]) > _last_new_item:
295 _last_new_item = int(item["index"])
296 saveLocaldata()
297
298 var full_item_name = item["text"]
299 if prog_id != null:
300 var door = gamedata.objects.get_doors()[prog_id]
301 full_item_name = "%s (%s)" % [full_item_name, door.get_name()]
302
303 var message
304 if "sender" in item:
305 message = (
306 "Received %s from %s"
307 % [wrapInItemColorTags(full_item_name, item["flags"]), item["sender"]]
308 )
309 else:
310 message = "Found %s" % wrapInItemColorTags(full_item_name, item["flags"])
311
312 if gamedata.anti_trap_ids.has(item):
313 keyboard.block_letter(gamedata.anti_trap_ids[item])
314
315 global._print(message)
316
317 global.get_node("Messages").showMessage(message)
318
319
320func _process_item_sent_notification(message):
321 var sentMsg = (
322 "Sent %s to %s"
323 % [
324 wrapInItemColorTags(message["item_name"], message["item_flags"]),
325 message["receiver_name"]
326 ]
327 )
328 #if _hinted_locations.has(message["item"]["location"]):
329 # sentMsg += " ([color=#fafad2]Hinted![/color])"
330 global.get_node("Messages").showMessage(sentMsg)
331
332
333func _process_hint_received(message):
334 var is_for = ""
335 if message["self"] == 0:
336 is_for = " for %s" % message["receiver_name"]
337
338 global.get_node("Messages").showMessage(
339 (
340 "Hint: %s%s is on %s"
341 % [
342 wrapInItemColorTags(message["item_name"], message["item_flags"]),
343 is_for,
344 message["location_name"]
345 ]
346 )
347 )
348
349
350func _process_text_message(message):
351 var parts = []
352 for message_part in message:
353 if message_part["type"] == "text":
354 parts.append(message_part["text"])
355 elif message_part["type"] == "player":
356 if message_part["self"] == 1:
357 parts.append("[color=#ee00ee]%s[/color]" % message_part["text"])
358 else:
359 parts.append("[color=#fafad2]%s[/color]" % message_part["text"])
360 elif message_part["type"] == "item":
361 parts.append(wrapInItemColorTags(message_part["text"], int(message_part["flags"])))
362 elif message_part["type"] == "location":
363 parts.append("[color=#00ff7f]%s[/color]" % message_part["text"])
364
365 var textclient_node = global.get_node("Textclient")
366 if textclient_node != null:
367 textclient_node.parse_printjson("".join(parts))
368
369
370func _process_location_scout(location_id, item_name, player_name, flags, for_self):
371 _location_scouts[location_id] = {
372 "item": item_name, "player": player_name, "flags": flags, "for_self": for_self
373 }
374
375 if for_self and flags & 4 != 0:
376 # This is a trap for us, so let's not display it.
377 return
378
379 var gamedata = global.get_node("Gamedata")
380 var map_id = gamedata.map_id_by_name.get(global.map)
381
382 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
383 if letter_id != null:
384 var letter = gamedata.objects.get_letters()[letter_id]
385 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
386 if room.get_map_id() == map_id:
387 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
388 letter.get_path()
389 )
390 if collectable != null:
391 collectable.setScoutedText(item_name)
392
393
394func _on_accessible_locations_updated():
395 var textclient_node = global.get_node("Textclient")
396 if textclient_node != null:
397 textclient_node.update_locations()
398
399
400func _on_checked_locations_updated():
401 var textclient_node = global.get_node("Textclient")
402 if textclient_node != null:
403 textclient_node.update_locations(false)
404
405
406func _on_checked_worldports_updated():
407 var textclient_node = global.get_node("Textclient")
408 if textclient_node != null:
409 textclient_node.update_locations()
410 textclient_node.update_worldports()
411
412
413func _on_ignored_locations_updated(locations):
414 _ignored_locations = locations
415
416 var textclient_node = global.get_node("Textclient")
417 if textclient_node != null:
418 textclient_node.update_locations()
419
420
421func _on_hinted_locations_updated():
422 var textclient_node = global.get_node("Textclient")
423 if textclient_node != null:
424 textclient_node.update_locations()
425
426
427func _on_door_latched(door_id):
428 var gamedata = global.get_node("Gamedata")
429 if gamedata.get_door_map_name(door_id) != global.map:
430 return
431
432 var receivers = gamedata.get_door_receivers(door_id)
433 var scene = get_tree().get_root().get_node_or_null("scene")
434 if scene != null:
435 for receiver in receivers:
436 var rnode = scene.get_node_or_null(receiver)
437 if rnode != null:
438 rnode.handleTriggered()
439
440
441func _client_could_not_connect(message):
442 could_not_connect.emit(message)
443
444 if global.loaded:
445 var effects = global.get_node("Effects")
446 effects.set_connection_lost(true)
447
448 var messages = global.get_node("Messages")
449 messages.showMessage("Connection to multiworld lost.")
450
451
452func _client_connect_status(message):
453 connect_status.emit(message)
454
455
456func _client_connected(slot_data):
457 var effects = global.get_node("Effects")
458 effects.set_connection_lost(false)
459
460 if _already_connected:
461 var messages = global.get_node("Messages")
462 messages.showMessage("Reconnected to multiworld!")
463 return
464
465 _already_connected = true
466
467 var gamedata = global.get_node("Gamedata")
468
469 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
470 _last_new_item = -1
471
472 if FileAccess.file_exists(_localdata_file):
473 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
474 var localdata = []
475 if ap_file != null:
476 localdata = ap_file.get_var(true)
477 ap_file.close()
478
479 if typeof(localdata) != TYPE_ARRAY:
480 print("AP localdata file is corrupted")
481 localdata = []
482
483 if localdata.size() > 0:
484 _last_new_item = localdata[0]
485
486 # Read slot data.
487 custom_mint_ending = slot_data.get("custom_mint_ending", "")
488 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
489 daedalus_only = bool(slot_data.get("daedalus_only", false))
490 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
491 enable_gift_maps = slot_data.get("enable_gift_maps", [])
492 enable_icarus = bool(slot_data.get("enable_icarus", false))
493 endings_requirement = int(slot_data.get("endings_requirement", 0))
494 fast_travel_access = int(slot_data.get("fast_travel_access", 0))
495 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
496 masteries_requirement = int(slot_data.get("masteries_requirement", 0))
497 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
498 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
499 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
500 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
501 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
502 shuffle_worldports = bool(slot_data.get("shuffle_worldports", false))
503 strict_cyan_ending = bool(slot_data.get("strict_cyan_ending", false))
504 strict_purple_ending = bool(slot_data.get("strict_purple_ending", false))
505 victory_condition = int(slot_data.get("victory_condition", 0))
506
507 if slot_data.has("version"):
508 var version_msg = slot_data["version"]
509 apworld_version = [int(version_msg[0]), int(version_msg[1]), 0]
510 if version_msg.size() > 2:
511 apworld_version[2] = int(version_msg[2])
512
513 port_pairings.clear()
514 if slot_data.has("port_pairings"):
515 var raw_pp = slot_data.get("port_pairings")
516
517 for p1 in raw_pp.keys():
518 port_pairings[gamedata.port_id_by_ap_id[int(p1)]] = gamedata.port_id_by_ap_id[int(
519 raw_pp[p1]
520 )]
521
522 rte_mapping.clear()
523 if slot_data.has("rte"):
524 rte_mapping = slot_data.get("rte")
525
526 slot_rng = RandomNumberGenerator.new()
527 slot_rng.seed = int(slot_data.get("seed", 0))
528
529 music_mapping.clear()
530 if bool(slot_data.get("shuffle_music", false)):
531 for map_name in global.reserved_scenes:
532 var track_index = slot_rng.randi_range(0, musicPlayer.all_tracks.size() - 1)
533 music_mapping[map_name] = musicPlayer.all_tracks.keys()[track_index]
534
535 # Set up item locks.
536 _item_locks = {}
537
538 if shuffle_doors or daedalus_only:
539 for door in gamedata.objects.get_doors():
540 if (
541 door.get_type() != gamedata.SCRIPT_proto.DoorType.STANDARD
542 and door.get_type() != gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
543 ):
544 continue
545
546 if (
547 not shuffle_doors
548 and not (
549 daedalus_only
550 and door.has_daedalus_only_always_item()
551 and door.get_daedalus_only_always_item()
552 )
553 ):
554 continue
555
556 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
557
558 if shuffle_doors:
559 for progressive in gamedata.objects.get_progressives():
560 for i in range(0, progressive.get_doors().size()):
561 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
562 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
563
564 for door_group in gamedata.objects.get_door_groups():
565 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR:
566 if shuffle_worldports:
567 continue
568 elif door_group.get_type() != gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP:
569 continue
570
571 if (
572 not shuffle_doors
573 and not (
574 daedalus_only
575 and door_group.has_daedalus_only_always_item()
576 and door_group.get_daedalus_only_always_item()
577 )
578 ):
579 continue
580
581 for door in door_group.get_doors():
582 _item_locks[door] = [door_group.get_ap_id(), 1]
583
584 if shuffle_control_center_colors:
585 for door in gamedata.objects.get_doors():
586 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
587 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
588
589 for door_group in gamedata.objects.get_door_groups():
590 if (
591 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR
592 and not shuffle_worldports
593 ):
594 for door in door_group.get_doors():
595 _item_locks[door] = [door_group.get_ap_id(), 1]
596
597 if shuffle_gallery_paintings:
598 for door in gamedata.objects.get_doors():
599 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
600 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
601
602 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
603 for door_group in gamedata.objects.get_door_groups():
604 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
605 for door in door_group.get_doors():
606 if not _item_locks.has(door):
607 _item_locks[door] = [door_group.get_ap_id(), 1]
608
609 # Create a reverse item locks map for processing items.
610 _inverse_item_locks = {}
611
612 for door_id in _item_locks.keys():
613 var lock = _item_locks.get(door_id)
614
615 if not _inverse_item_locks.has(lock[0]):
616 _inverse_item_locks[lock[0]] = []
617
618 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
619
620 if shuffle_worldports:
621 var textclient = global.get_node("Textclient")
622 textclient.setup_worldports()
623
624 ap_connected.emit()
625
626
627func start_batching_locations():
628 _batch_locations = true
629
630
631func send_location(loc_id):
632 if client._checked_locations.has(loc_id):
633 return
634
635 if _batch_locations:
636 _held_locations.append(loc_id)
637 else:
638 client.sendLocation(loc_id)
639
640
641func scout_location(loc_id):
642 if _location_scouts.has(loc_id):
643 return _location_scouts.get(loc_id)
644
645 if _batch_locations:
646 _held_location_scouts.append(loc_id)
647 else:
648 client.scoutLocation(loc_id)
649
650 return null
651
652
653func stop_batching_locations():
654 _batch_locations = false
655
656 if not _held_locations.is_empty():
657 client.sendLocations(_held_locations)
658 _held_locations.clear()
659
660 if not _held_location_scouts.is_empty():
661 client.scoutLocations(_held_location_scouts)
662 _held_location_scouts.clear()
663
664
665func colorForItemType(flags):
666 var int_flags = int(flags)
667 if int_flags & 1: # progression
668 if int_flags & 2: # proguseful
669 return "#f0d200"
670 else:
671 return "#bc51e0"
672 elif int_flags & 2: # useful
673 return "#2b67ff"
674 elif int_flags & 4: # trap
675 return "#d63a22"
676 else: # filler
677 return "#14de9e"
678
679
680func wrapInItemColorTags(text, flags):
681 var int_flags = int(flags)
682 if int_flags & 1 and int_flags & 2: # proguseful
683 return "[rainbow]%s[/rainbow]" % text
684 else:
685 return "[color=%s]%s[/color]" % [colorForItemType(flags), text]
686
687
688func get_letter_behavior(key, level2):
689 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
690 return kLETTER_BEHAVIOR_UNLOCKED
691
692 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
693 if level2:
694 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
695 return kLETTER_BEHAVIOR_VANILLA
696 else:
697 return kLETTER_BEHAVIOR_ITEM
698 else:
699 return kLETTER_BEHAVIOR_UNLOCKED
700
701 if not level2 and ["h", "i", "n", "t"].has(key):
702 # This differs from the equivalent function in the apworld. Logically it is
703 # the same as UNLOCKED since they are in the starting room, but VANILLA
704 # means the player still has to actually pick up the letters.
705 return kLETTER_BEHAVIOR_VANILLA
706
707 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
708 return kLETTER_BEHAVIOR_ITEM
709
710 return kLETTER_BEHAVIOR_VANILLA
711
712
713func setup_keys():
714 keyboard.load_seed()
715
716 _letters_setup = true
717
718 for k in _held_letters.keys():
719 _process_key_item(k, _held_letters[k])
720
721 _held_letters.clear()
722
723
724func _process_key_item(key, level):
725 if not _letters_setup:
726 _held_letters[key] = max(_held_letters.get(key, 0), level)
727 return
728
729 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
730 level += 1
731
732 keyboard.collect_remote_letter(key, level)
733
734
735func update_job_well_done_sign():
736 if global.map != "daedalus":
737 return
738
739 var gamedata = global.get_node("Gamedata")
740 var job_item = gamedata.objects.get_special_ids()["A Job Well Done"]
741 var jobs_done = client.getItemAmount(job_item)
742
743 var sign2 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign2")
744 var sign3 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign3")
745
746 if sign2 != null and sign3 != null:
747 if jobs_done == 0:
748 sign2.text = "what are you doing"
749 sign3.text = "?"
750 elif jobs_done == 1:
751 sign2.text = "a job well done"
752 sign3.text = "is its own reward"
753 else:
754 sign2.text = "%d jobs well done" % jobs_done
755 sign3.text = "are their own reward"
756
757 sign2.get_node("MeshInstance3D").mesh.text = sign2.text
758 sign3.get_node("MeshInstance3D").mesh.text = sign3.text
759
760
761func toggle_ignored_location(loc_id):
762 if loc_id in _ignored_locations:
763 client.removeIgnoredLocation(loc_id)
764 else:
765 client.addIgnoredLocation(loc_id)
766
767
768func get_map_script(map_name):
769 if !_map_scripts.has(map_name):
770 var runtime = global.get_node("Runtime")
771 var script_path = "maps/%s.gd" % map_name
772 if runtime.path_exists(script_path):
773 var script = runtime.load_script(script_path)
774 _map_scripts[map_name] = script.new()
775 else:
776 _map_scripts[map_name] = null
777
778 return _map_scripts[map_name]