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