From bb4b16ca06c70cf263424955713c91117f2f1813 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 6 Sep 2025 09:20:28 -0400 Subject: [Client] Handle letter shuffle Cyan doors are not handled properly yet. --- client/Archipelago/collectable.gd | 12 ++ client/Archipelago/gamedata.gd | 5 + client/Archipelago/keyHolder.gd | 38 ++++++ client/Archipelago/keyHolderChecker.gd | 24 ++++ client/Archipelago/keyHolderResetterListener.gd | 8 ++ client/Archipelago/keyboard.gd | 170 ++++++++++++++++++++++++ client/Archipelago/manager.gd | 74 ++++++++++- client/Archipelago/saver.gd | 6 +- client/Archipelago/settings_screen.gd | 23 ++-- 9 files changed, 345 insertions(+), 15 deletions(-) create mode 100644 client/Archipelago/collectable.gd create mode 100644 client/Archipelago/keyHolder.gd create mode 100644 client/Archipelago/keyHolderChecker.gd create mode 100644 client/Archipelago/keyHolderResetterListener.gd create mode 100644 client/Archipelago/keyboard.gd (limited to 'client/Archipelago') diff --git a/client/Archipelago/collectable.gd b/client/Archipelago/collectable.gd new file mode 100644 index 0000000..7bbdd8b --- /dev/null +++ b/client/Archipelago/collectable.gd @@ -0,0 +1,12 @@ +extends "res://scripts/nodes/collectable.gd" + + +func pickedUp(): + if unlock_type == "key": + var ap = global.get_node("Archipelago") + if ap.get_letter_behavior(unlock_key, level == 2) == ap.kLETTER_BEHAVIOR_VANILLA: + ap.keyboard.collect_local_letter(unlock_key, level) + else: + ap.keyboard.update_unlocks() + + super.pickedUp() diff --git a/client/Archipelago/gamedata.gd b/client/Archipelago/gamedata.gd index 669ad3d..11f4981 100644 --- a/client/Archipelago/gamedata.gd +++ b/client/Archipelago/gamedata.gd @@ -8,6 +8,7 @@ var painting_id_by_map_node_path = {} var door_id_by_ap_id = {} var map_id_by_name = {} var progressive_id_by_ap_id = {} +var letter_key_by_ap_id = {} func _init(proto_script): @@ -54,6 +55,10 @@ func load(data_bytes): for progressive in objects.get_progressives(): progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id() + for letter in objects.get_letters(): + if not letter.has_level2() or not letter.get_level2(): + letter_key_by_ap_id[letter.get_ap_id()] = letter.get_key() + func get_door_for_map_node_path(map_name, node_path): 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 @@ +extends "res://scripts/nodes/keyHolder.gd" + + +func setFromAp(key, level): + if level > 0: + has_key = true + is_complete = "%s%d" % [key, level] + held_key = key + held_level = level + get_node("Hinge/Letter").mesh.text = held_key + get_node("Hinge/Letter2").mesh.text = held_key + setMaterial() + emit_signal("trigger") + else: + has_key = false + held_key = "" + held_level = 0 + setMaterial() + get_node("Hinge/Letter").mesh.text = "-" + get_node("Hinge/Letter2").mesh.text = "-" + is_complete = "" + emit_signal("untrigger") + + +func addKey(key): + var node_path = String( + get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() + ) + var ap = global.get_node("Archipelago") + ap.keyboard.put_in_keyholder(key, global.map, node_path) + + +func removeKey(): + var node_path = String( + get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() + ) + var ap = global.get_node("Archipelago") + 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 @@ +extends "res://scripts/nodes/listeners/keyHolderChecker.gd" + + +func check(): + var ap = global.get_node("Archipelago") + var matches = [] + for map in ap.keyboard.keyholder_state.keys(): + var nodes = ap.keyboard.keyholder_state[map] + for node in nodes.keys(): + matches.append([nodes[node], 1, map, "/root/scene/%s" % node]) + + var count = 0 + for key_match in matches: + var active = ( + key_match[2] + String(key_match[3]).replace("/root/scene/Components/KeyHolders/", ".") + ) + if map[active] == key_match[0]: + emit_signal("trigger_letter", key_match[0], true) + count += 1 + else: + emit_signal("trigger_letter", key_match[0], false) + + if count > 25: + 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 @@ +extends "res://scripts/nodes/listeners/keyHolderResetterListener.gd" + + +func reset(): + var ap = global.get_node("Archipelago") + var was_removed = ap.keyboard.reset_keyholders() + if was_removed: + sfxPlayer.sfx_play("pickup") diff --git a/client/Archipelago/keyboard.gd b/client/Archipelago/keyboard.gd new file mode 100644 index 0000000..e43ec9f --- /dev/null +++ b/client/Archipelago/keyboard.gd @@ -0,0 +1,170 @@ +extends Node + +const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz" + +var letters_saved = {} +var letters_in_keyholders = [] +var letters_dynamic = {} +var keyholder_state = {} + +var filename = "" + + +func _init(): + reset() + + +func reset(): + letters_saved.clear() + letters_in_keyholders.clear() + letters_dynamic.clear() + keyholder_state.clear() + + +func load_seed(): + var ap = global.get_node("Archipelago") + + reset() + + filename = "user://archipelago_keys/%s_%d" % [ap.client._seed, ap.client._slot] + + if FileAccess.file_exists(filename): + var ap_file = FileAccess.open(filename, FileAccess.READ) + var localdata = [] + if ap_file != null: + localdata = ap_file.get_var(true) + ap_file.close() + + if typeof(localdata) != TYPE_ARRAY: + print("AP keyboard file is corrupted") + localdata = [] + + if localdata.size() > 0: + letters_saved = localdata[0] + if localdata.size() > 1: + letters_in_keyholders = localdata[1] + if localdata.size() > 2: + keyholder_state = localdata[2] + + for k in kALL_LETTERS: + var level = 0 + + if ap.get_letter_behavior(k, false) == ap.kLETTER_BEHAVIOR_UNLOCKED: + level += 1 + if ap.get_letter_behavior(k, true) == ap.kLETTER_BEHAVIOR_UNLOCKED: + level += 1 + + letters_dynamic[k] = level + + update_unlocks() + + +func save(): + var dir = DirAccess.open("user://") + var folder = "archipelago_keys" + if not dir.dir_exists(folder): + dir.make_dir(folder) + + var file = FileAccess.open(filename, FileAccess.WRITE) + + var data = [ + letters_saved, + letters_in_keyholders, + keyholder_state, + ] + file.store_var(data, true) + file.close() + + +func update_unlocks(): + unlocks.resetKeys() + + for k in kALL_LETTERS: + var level = 0 + + if not letters_in_keyholders.has(k): + level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0) + + if level > 2: + level = 2 + + unlocks.unlockKey(k, level) + + +func collect_local_letter(key, level): + if level < 0 or level > 2 or level < letters_saved.get(key, 0): + return + + letters_saved[key] = level + + update_unlocks() + save() + + +func collect_remote_letter(key, level): + if level < 0 or level > 2 or level < letters_dynamic.get(key, 0): + return + + letters_dynamic[key] = level + + update_unlocks() + save() + + +func put_in_keyholder(key, map, kh_path): + if not keyholder_state.has(map): + keyholder_state[map] = {} + + keyholder_state[map][kh_path] = key + letters_in_keyholders.append(key) + + get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp( + key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2) + ) + + update_unlocks() + save() + + +func remove_from_keyholder(key, map, kh_path): + if not keyholder_state.has(map): + # This... shouldn't happen. + keyholder_state[map] = {} + + keyholder_state[map].erase(kh_path) + letters_in_keyholders.erase(key) + + get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(key, 0) + + update_unlocks() + save() + + +func load_keyholders(map): + if keyholder_state.has(map): + var khs = keyholder_state[map] + + for path in khs.keys(): + var key = khs[path] + get_tree().get_root().get_node("scene").get_node(path).setFromAp( + key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2) + ) + + +func reset_keyholders(): + if letters_in_keyholders.is_empty(): + return false + + if keyholder_state.has(global.map): + for path in keyholder_state[global.map]: + get_tree().get_root().get_node("scene").get_node(path).setFromAp( + keyholder_state[global.map][path], 0 + ) + + keyholder_state.clear() + letters_in_keyholders.clear() + + update_unlocks() + save() + + return true diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd index 609e645..07d28a4 100644 --- a/client/Archipelago/manager.gd +++ b/client/Archipelago/manager.gd @@ -3,6 +3,7 @@ extends Node const my_version = "0.1.0" var SCRIPT_client +var SCRIPT_keyboard var SCRIPT_locationListener var SCRIPT_uuid var SCRIPT_victoryListener @@ -13,16 +14,30 @@ var ap_pass = "" var connection_history = [] var client +var keyboard var _localdata_file = "" var _last_new_item = -1 var _batch_locations = false var _held_locations = [] var _item_locks = {} +var _held_letters = {} +var _letters_setup = false + +const kSHUFFLE_LETTERS_VANILLA = 0 +const kSHUFFLE_LETTERS_UNLOCKED = 1 +const kSHUFFLE_LETTERS_PROGRESSIVE = 2 +const kSHUFFLE_LETTERS_VANILLA_CYAN = 3 +const kSHUFFLE_LETTERS_ITEM_CYAN = 4 + +const kLETTER_BEHAVIOR_VANILLA = 0 +const kLETTER_BEHAVIOR_ITEM = 1 +const kLETTER_BEHAVIOR_UNLOCKED = 2 var daedalus_roof_access = false var keyholder_sanity = false var shuffle_doors = false +var shuffle_letters = kSHUFFLE_LETTERS_VANILLA var victory_condition = -1 signal could_not_connect @@ -66,6 +81,9 @@ func _ready(): add_child(client) + keyboard = SCRIPT_keyboard.new() + add_child(keyboard) + func saveSettings(): # Save the AP settings to disk. @@ -100,6 +118,8 @@ func saveLocaldata(): func connectToServer(): _last_new_item = -1 + _letters_setup = false + _held_letters = {} client.connectToServer(ap_server, ap_user, ap_pass) @@ -140,11 +160,10 @@ func _process_item(item, index, from, flags, amount): var rnode = scene.get_node_or_null(receiver) if rnode != null: rnode.handleTriggered() - #for painting_id in gamedata.objects.get_doors()[door_id].get_move_paintings(): - # var painting = gamedata.objects.get_paintings()[painting_id] - # var pnode = scene.get_node_or_null(painting.get_path() + "/teleportListener") - # if pnode != null: - # pnode.handleTriggered() + + var letter_key = gamedata.letter_key_by_ap_id.get(item, null) + if letter_key != null: + _process_key_item(letter_key, amount) # Show a message about the item if it's new. if index != null and index > _last_new_item: @@ -293,6 +312,7 @@ func _client_connected(slot_data): daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) shuffle_doors = bool(slot_data.get("shuffle_doors", false)) + shuffle_letters = int(slot_data.get("shuffle_letters", 0)) victory_condition = int(slot_data.get("victory_condition", 0)) # Set up item locks. @@ -344,3 +364,47 @@ func colorForItemType(flags): return "#d63a22" else: # filler return "#14de9e" + + +func get_letter_behavior(key, level2): + if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED: + return kLETTER_BEHAVIOR_UNLOCKED + + if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters): + if level2: + if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN: + return kLETTER_BEHAVIOR_VANILLA + else: + return kLETTER_BEHAVIOR_ITEM + else: + return kLETTER_BEHAVIOR_UNLOCKED + + if not level2 and ["h", "i", "n", "t"].has(key): + # This differs from the equivalent function in the apworld. Logically it is + # the same as UNLOCKED since they are in the starting room, but VANILLA + # means the player still has to actually pick up the letters. + return kLETTER_BEHAVIOR_VANILLA + + if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE: + return kLETTER_BEHAVIOR_ITEM + + return kLETTER_BEHAVIOR_VANILLA + + +func setup_keys(): + keyboard.load_seed() + + _letters_setup = true + + for k in _held_letters.keys(): + _process_key_item(k, _held_letters[k]) + + _held_letters.clear() + + +func _process_key_item(key, level): + if not _letters_setup: + _held_letters[key] = max(_held_letters.get(key, 0), level) + return + + keyboard.collect_remote_letter(key, level) 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" func levelLoaded(): - reload.call_deferred() + if type == "keyholders": + var ap = global.get_node("Archipelago") + ap.keyboard.load_keyholders.call_deferred(global.map) + else: + 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(): var ap_instance = ap_script.new() ap_instance.name = "Archipelago" - #apclient_instance.SCRIPT_doorControl = load("user://maps/Archipelago/doorControl.gd") - #apclient_instance.SCRIPT_effects = load("user://maps/Archipelago/effects.gd") - #apclient_instance.SCRIPT_location = load("user://maps/Archipelago/location.gd") - #apclient_instance.SCRIPT_mypainting = load("user://maps/Archipelago/mypainting.gd") - #apclient_instance.SCRIPT_panel = load("user://maps/Archipelago/panel.gd") - #apclient_instance.SCRIPT_textclient = load("user://maps/Archipelago/textclient.gd") - ap_instance.SCRIPT_client = load("user://maps/Archipelago/client.gd") + ap_instance.SCRIPT_keyboard = load("user://maps/Archipelago/keyboard.gd") ap_instance.SCRIPT_locationListener = load("user://maps/Archipelago/locationListener.gd") ap_instance.SCRIPT_uuid = load("user://maps/Archipelago/vendor/uuid.gd") ap_instance.SCRIPT_victoryListener = load("user://maps/Archipelago/victoryListener.gd") @@ -38,7 +32,13 @@ func _ready(): # Let's also inject any scripts we need to inject now. installScriptExtension(ResourceLoader.load("user://maps/Archipelago/animationListener.gd")) + installScriptExtension(ResourceLoader.load("user://maps/Archipelago/collectable.gd")) installScriptExtension(ResourceLoader.load("user://maps/Archipelago/door.gd")) + installScriptExtension(ResourceLoader.load("user://maps/Archipelago/keyHolder.gd")) + installScriptExtension(ResourceLoader.load("user://maps/Archipelago/keyHolderChecker.gd")) + installScriptExtension( + ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd") + ) installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd")) installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd")) installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd")) @@ -141,17 +141,22 @@ func connectionSuccessful(): global.universe = "lingo" global.map = "the_entry" - unlocks.resetKeys() unlocks.resetCollectables() unlocks.resetData() - unlocks.loadKeys() + + ap.setup_keys() + unlocks.loadCollectables() unlocks.loadData() unlocks.unlockKey("capslock", 1) clearResourceCache("res://objects/meshes/gridDoor.tscn") + clearResourceCache("res://objects/nodes/collectable.tscn") clearResourceCache("res://objects/nodes/door.tscn") + clearResourceCache("res://objects/nodes/keyHolder.tscn") clearResourceCache("res://objects/nodes/listeners/animationListener.tscn") + clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn") + clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn") clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") clearResourceCache("res://objects/nodes/player.tscn") -- cgit 1.4.1