From ea87cbbe4a23ceff72f31e461c7ead32f560031e Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 25 Oct 2025 11:20:55 -0400 Subject: Made White Ending customizable --- apworld/__init__.py | 2 + apworld/client/gamedata.gd | 6 ++- apworld/client/main.gd | 6 +++ apworld/client/manager.gd | 4 ++ apworld/client/player.gd | 127 +++++++++++++++++++++++++++++++++++++++++++++ apworld/options.py | 22 ++++++++ apworld/player_logic.py | 29 +++++++---- apworld/static_logic.py | 2 +- 8 files changed, 187 insertions(+), 11 deletions(-) (limited to 'apworld') diff --git a/apworld/__init__.py b/apworld/__init__.py index 4ebf845..f5774c6 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -132,7 +132,9 @@ class Lingo2World(World): "daedalus_roof_access", "enable_gift_maps", "enable_icarus", + "endings_requirement", "keyholder_sanity", + "masteries_requirement", "shuffle_control_center_colors", "shuffle_doors", "shuffle_gallery_paintings", diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 9305003..3a35125 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd @@ -221,7 +221,11 @@ func _get_generated_door_location_name(door): if door.get_type() != SCRIPT_proto.DoorType.STANDARD: return null - if door.get_keyholders().size() > 0 or door.get_endings().size() > 0 or door.has_complete_at(): + if ( + door.get_keyholders().size() > 0 + or (door.has_white_ending() and door.get_white_ending()) + or door.has_complete_at() + ): return null if door.get_panels().size() > 4: diff --git a/apworld/client/main.gd b/apworld/client/main.gd index 1d0df1f..a3b21c5 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd @@ -83,6 +83,12 @@ func _ready(): compass_overlay_instance.SCRIPT_compass = runtime.load_script("compass.gd") global.add_child(compass_overlay_instance) + unlocks.data["advanced_mastery"] = "" + unlocks.data["charismatic_mastery"] = "" + unlocks.data["crystalline_mastery"] = "" + unlocks.data["icarus_mastery"] = "" + unlocks.data["stellar_mastery"] = "" + var ap = global.get_node("Archipelago") var gamedata = global.get_node("Gamedata") ap.ap_connected.connect(connectionSuccessful) diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 41ab648..399d6a5 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -65,7 +65,9 @@ var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 var daedalus_roof_access = false var enable_gift_maps = [] var enable_icarus = false +var endings_requirement = 0 var keyholder_sanity = false +var masteries_requirement = 0 var port_pairings = {} var shuffle_control_center_colors = false var shuffle_doors = false @@ -443,7 +445,9 @@ func _client_connected(slot_data): daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) enable_gift_maps = slot_data.get("enable_gift_maps", []) enable_icarus = bool(slot_data.get("enable_icarus", false)) + endings_requirement = int(slot_data.get("endings_requirement", 0)) keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) + masteries_requirement = int(slot_data.get("masteries_requirement", 0)) shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) shuffle_doors = bool(slot_data.get("shuffle_doors", false)) shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false)) diff --git a/apworld/client/player.gd b/apworld/client/player.gd index 789d1b7..9acb942 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd @@ -19,6 +19,84 @@ func _ready(): ap.start_batching_locations() + if global.map == "control_center": + get_node("/root/scene/Components/Doors/entry_18").queue_free() + + _set_up_mastery_listener("advanced") + _set_up_mastery_listener("charismatic") + _set_up_mastery_listener("crystalline") + _set_up_mastery_listener("icarus") + _set_up_mastery_listener("stellar") + + if ap.endings_requirement != 12 or ap.masteries_requirement != 0: + # Set up listeners for the potential White Ending requirements. + var merging_prefab = preload("res://objects/nodes/listeners/mergingListener.tscn") + + var old_door = get_node("/root/scene/Components/Doors/entry_19") + var new_door = old_door.duplicate() + new_door.name = "entry_19_new" + new_door.senders.clear() + new_door.senderGroup.clear() + new_door.excludeSenders.clear() + + if ap.endings_requirement == 12: + new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners")) + elif ap.endings_requirement > 0: + if ap.masteries_requirement == 0: + new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners")) + new_door.complete_at = ap.endings_requirement + else: + var endings_merge = merging_prefab.instantiate() + endings_merge.name = "EndingsMerge" + endings_merge.senderGroup.append( + NodePath("/root/scene/Meshes/Trophies/Listeners") + ) + endings_merge.complete_at = ap.endings_requirement + get_node("/root/scene/Components").add_child.call_deferred(endings_merge) + new_door.senders.append(NodePath("/root/scene/Components/EndingsMerge")) + + var max_masteries = 13 + ap.enable_gift_maps.size() + if ap.enable_icarus: + max_masteries += 1 + + if ap.masteries_requirement == max_masteries: + new_door.senderGroup.append( + NodePath("/root/scene/Meshes/Trophies/MasteryListeners") + ) + new_door.excludeSenders.append( + NodePath( + "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite" + ) + ) + elif ap.masteries_requirement > 0: + if ap.endings_requirement == 0: + new_door.senderGroup.append( + NodePath("/root/scene/Meshes/Trophies/MasteryListeners") + ) + new_door.excludeSenders.append( + NodePath( + "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite" + ) + ) + new_door.complete_at = ap.masteries_requirement + else: + var masteries_merge = merging_prefab.instantiate() + masteries_merge.name = "MasteriesMerge" + masteries_merge.senderGroup.append( + NodePath("/root/scene/Meshes/Trophies/MasteryListeners") + ) + masteries_merge.excludeSenders.append( + NodePath( + "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite" + ) + ) + masteries_merge.complete_at = ap.masteries_requirement + get_node("/root/scene/Components").add_child.call_deferred(masteries_merge) + new_door.senders.append(NodePath("/root/scene/Components/MasteriesMerge")) + + old_door.queue_free() + get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door) + # Block off roof access in Daedalus. if global.map == "daedalus" and not ap.daedalus_roof_access: _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49) @@ -297,6 +375,7 @@ func _ready(): var collectable_prefab = preload("res://objects/nodes/collectable.tscn") var saver_prefab = preload("res://objects/nodes/saver.tscn") var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn") + var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn") var mastery = collectable_prefab.instantiate() mastery.name = "collectable" @@ -314,6 +393,13 @@ func _ready(): tpl.nested = true mastery.add_child.call_deferred(tpl) + var usl = usl_prefab.instantiate() + usl.name = "unlockSetterListenerMastery" + usl.key = "icarus_mastery" + usl.value = "unlocked" + usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable")) + get_node("/root/scene/Components").add_child.call_deferred(usl) + var saver = saver_prefab.instantiate() saver.name = "saver_collectables" saver.type = "collectables" @@ -325,6 +411,7 @@ func _ready(): var collectable_prefab = preload("res://objects/nodes/collectable.tscn") var saver_prefab = preload("res://objects/nodes/saver.tscn") var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn") + var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn") var mastery = collectable_prefab.instantiate() mastery.name = "collectable" @@ -343,6 +430,13 @@ func _ready(): tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_31")) mastery.add_child.call_deferred(tpl) + var usl = usl_prefab.instantiate() + usl.name = "unlockSetterListenerMastery" + usl.key = "advanced_mastery" + usl.value = "unlocked" + usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable")) + get_node("/root/scene/Components").add_child.call_deferred(usl) + var saver = saver_prefab.instantiate() saver.name = "saver_collectables" saver.type = "collectables" @@ -353,6 +447,7 @@ func _ready(): if global.map == "the_charismatic": var collectable_prefab = preload("res://objects/nodes/collectable.tscn") var saver_prefab = preload("res://objects/nodes/saver.tscn") + var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn") var mastery = collectable_prefab.instantiate() mastery.name = "collectable" @@ -362,6 +457,13 @@ func _ready(): mastery.material_override = load("res://assets/materials/gold.material") get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery) + var usl = usl_prefab.instantiate() + usl.name = "unlockSetterListenerMastery" + usl.key = "charismatic_mastery" + usl.value = "unlocked" + usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable")) + get_node("/root/scene/Components").add_child.call_deferred(usl) + var saver = saver_prefab.instantiate() saver.name = "saver_collectables" saver.type = "collectables" @@ -373,6 +475,7 @@ func _ready(): var collectable_prefab = preload("res://objects/nodes/collectable.tscn") var saver_prefab = preload("res://objects/nodes/saver.tscn") var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn") + var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn") var mastery = collectable_prefab.instantiate() mastery.name = "collectable" @@ -389,6 +492,13 @@ func _ready(): tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_3")) mastery.add_child.call_deferred(tpl) + var usl = usl_prefab.instantiate() + usl.name = "unlockSetterListenerMastery" + usl.key = "crystalline_mastery" + usl.value = "unlocked" + usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable")) + get_node("/root/scene/Components").add_child.call_deferred(usl) + var saver = saver_prefab.instantiate() saver.name = "saver_collectables" saver.type = "collectables" @@ -399,6 +509,7 @@ func _ready(): if global.map == "the_stellar": var collectable_prefab = preload("res://objects/nodes/collectable.tscn") var saver_prefab = preload("res://objects/nodes/saver.tscn") + var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn") var collectables = Node.new() collectables.name = "Collectables" @@ -412,6 +523,13 @@ func _ready(): collectables.add_child.call_deferred(mastery) get_node("/root/scene/Components").add_child.call_deferred(collectables) + var usl = usl_prefab.instantiate() + usl.name = "unlockSetterListenerMastery" + usl.key = "stellar_mastery" + usl.value = "unlocked" + usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable")) + get_node("/root/scene/Components").add_child.call_deferred(usl) + var saver = saver_prefab.instantiate() saver.name = "saver_collectables" saver.type = "collectables" @@ -585,5 +703,14 @@ func _set_up_invis_wall(x, y, z, sx, sy, sz): get_parent().add_child.call_deferred(newwall) +func _set_up_mastery_listener(name): + var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn") + var url = prefab.instantiate() + url.name = "unlockReaderListenerMastery_%s" % name + url.key = "%s_mastery" % name + url.value = "unlocked" + get_node("/root/scene/Meshes/Trophies/MasteryListeners").add_child.call_deferred(url) + + func _process(_dt): compass.update_rotation(global_rotation.y) diff --git a/apworld/options.py b/apworld/options.py index 7577e0c..a56b40d 100644 --- a/apworld/options.py +++ b/apworld/options.py @@ -181,6 +181,26 @@ class VictoryCondition(Choice): option_white_ending = 12 +class EndingsRequirement(Range): + """The number of endings required to unlock White Ending.""" + display_name = "Endings Requirement" + range_start = 0 + range_end = 12 + default = 12 + + +class MasteriesRequirement(Range): + """The number of masteries required to unlock White Ending. + + There are only 13 masteries in the base game, but some of the other slot options may add more masteries to the + world. If the chosen number of masteries is higher than the total in your world, it will be automatically lowered to + the maximum.""" + display_name = "Masteries Requirement" + range_start = 0 + range_end = 18 + default = 0 + + class TrapPercentage(Range): """Replaces junk items with traps, at the specified rate.""" display_name = "Trap Percentage" @@ -205,4 +225,6 @@ class Lingo2Options(PerGameCommonOptions): strict_purple_ending: StrictPurpleEnding strict_cyan_ending: StrictCyanEnding victory_condition: VictoryCondition + endings_requirement: EndingsRequirement + masteries_requirement: MasteriesRequirement trap_percentage: TrapPercentage diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 67365b7..e21e2c3 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -249,6 +249,13 @@ class Lingo2PlayerLogic: self.shuffled_maps = set(game_map.id for game_map in world.static_logic.objects.maps if should_shuffle_map(game_map)) + maximum_masteries = 13 + len(world.options.enable_gift_maps.value) + if world.options.enable_icarus: + maximum_masteries += 1 + + if world.options.masteries_requirement > maximum_masteries: + world.options.masteries_requirement.value = maximum_masteries + if self.world.options.shuffle_doors: for progressive in world.static_logic.objects.progressives: for i in range(0, len(progressive.doors)): @@ -362,18 +369,23 @@ class Lingo2PlayerLogic: self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, AccessRequirements())) + if world.options.masteries_requirement > 0: + event_name = f"{world.static_logic.get_room_object_map_name(mastery)} - Mastery (Collected)" + self.event_loc_item_by_room.setdefault(mastery.room_id, {})[event_name] = "Mastery" + for ending in world.static_logic.objects.endings: if world.static_logic.get_room_object_map_id(ending) not in self.shuffled_maps: continue - # Don't create a location for your selected ending, and never create a location for White Ending. + # Don't create a location for your selected ending. Also don't create a location for White Ending if it's + # necessarily in the postgame, i.e. it requires all 12 other endings. if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\ - and ending.name != "WHITE": + and (ending.name != "WHITE" or world.options.endings_requirement < 12): self.locations_by_room.setdefault(ending.room_id, []).append(PlayerLocation(ending.ap_id, AccessRequirements())) event_name = f"{ending.name.capitalize()} Ending (Achieved)" - item_name = event_name + item_name = "Ending" if world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name: item_name = "Victory" @@ -520,13 +532,12 @@ class Lingo2PlayerLogic: for room in door.rooms: reqs.rooms.add(self.world.static_logic.get_room_region_name(room)) - for ending_id in door.endings: - ending = self.world.static_logic.objects.endings[ending_id] + if door.white_ending: + if self.world.options.endings_requirement > 0: + reqs.progressives["Ending"] = self.world.options.endings_requirement.value - if self.world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name: - reqs.items.add("Victory") - else: - reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)") + if self.world.options.masteries_requirement > 0: + reqs.progressives["Mastery"] = self.world.options.masteries_requirement.value for sub_door_id in door.doors: sub_reqs = self.get_door_open_reqs(sub_door_id) diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 2546007..fb23e4c 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py @@ -105,7 +105,7 @@ class Lingo2StaticLogic: if door.type != data_pb2.DoorType.STANDARD: return None - if len(door.keyholders) > 0 or len(door.endings) > 0 or door.HasField("complete_at"): + if len(door.keyholders) > 0 or door.white_ending or door.HasField("complete_at"): return None if len(door.panels) > 4: -- cgit 1.4.1