From 8cfee29fe6500e96c3b8669c12ac944716e9aae4 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 23 Oct 2025 12:13:09 -0400 Subject: Added "Enable Gift Maps" option Only supports The Advanced so far. Also added the mastery to The Advanced. Location listeners are now created after any map edits are made since some locations may require custom nodes (like The Advanced's mastery). --- apworld/__init__.py | 1 + apworld/client/manager.gd | 2 + apworld/client/player.gd | 338 ++++++++++++++++++++++++++++------------------ apworld/options.py | 20 ++- apworld/player_logic.py | 3 + 5 files changed, 228 insertions(+), 136 deletions(-) diff --git a/apworld/__init__.py b/apworld/__init__.py index 6540b08..4ebf845 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -130,6 +130,7 @@ class Lingo2World(World): slot_options = [ "cyan_door_behavior", "daedalus_roof_access", + "enable_gift_maps", "enable_icarus", "keyholder_sanity", "shuffle_control_center_colors", diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index a17bee8..91797b2 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -63,6 +63,7 @@ const kEndingNameByVictoryValue = { var apworld_version = [0, 0, 0] var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 var daedalus_roof_access = false +var enable_gift_maps = [] var keyholder_sanity = false var port_pairings = {} var shuffle_control_center_colors = false @@ -439,6 +440,7 @@ func _client_connected(slot_data): # Read slot data. cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) + enable_gift_maps = slot_data.get("enable_gift_maps", []) keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) shuffle_doors = bool(slot_data.get("shuffle_doors", false)) diff --git a/apworld/client/player.gd b/apworld/client/player.gd index 1330e24..43bf758 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd @@ -19,141 +19,6 @@ func _ready(): ap.start_batching_locations() - # Set up door locations. - var map_id = gamedata.map_id_by_name.get(global.map) - for door in gamedata.objects.get_doors(): - if door.get_map_id() != map_id: - continue - - if not door.has_ap_id(): - continue - - if ( - door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY - or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING - or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR - ): - continue - - var locationListener = ap.SCRIPT_locationListener.new() - locationListener.location_id = door.get_ap_id() - locationListener.name = "locationListener_%d" % door.get_ap_id() - - for panel_ref in door.get_panels(): - var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()] - var panel_path = panel_data.get_path() - - if panel_ref.has_answer(): - for proxy in panel_data.get_proxies(): - if proxy.get_answer() == panel_ref.get_answer(): - panel_path = proxy.get_path() - break - - locationListener.senders.append(NodePath("/root/scene/" + panel_path)) - - for keyholder_ref in door.get_keyholders(): - var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()] - - var khl = khl_script.new() - khl.name = ( - "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()] - ) - khl.answer = keyholder_ref.get_key() - khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path())) - get_parent().add_child.call_deferred(khl) - - locationListener.senders.append(NodePath("../" + khl.name)) - - for sender in door.get_senders(): - locationListener.senders.append(NodePath("/root/scene/" + sender)) - - if door.has_complete_at(): - locationListener.complete_at = door.get_complete_at() - - get_parent().add_child.call_deferred(locationListener) - - # Set up letter locations. - for letter in gamedata.objects.get_letters(): - var room = gamedata.objects.get_rooms()[letter.get_room_id()] - if room.get_map_id() != map_id: - continue - - var locationListener = ap.SCRIPT_locationListener.new() - locationListener.location_id = letter.get_ap_id() - locationListener.name = "locationListener_%d" % letter.get_ap_id() - locationListener.senders.append(NodePath("/root/scene/" + letter.get_path())) - - get_parent().add_child.call_deferred(locationListener) - - if ( - ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2()) - != ap.kLETTER_BEHAVIOR_VANILLA - ): - var scout = ap.scout_location(letter.get_ap_id()) - if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0): - var collectable = get_tree().get_root().get_node("scene").get_node_or_null( - letter.get_path() - ) - if collectable != null: - collectable.setScoutedText.call_deferred(scout["item"]) - - # Set up mastery locations. - for mastery in gamedata.objects.get_masteries(): - var room = gamedata.objects.get_rooms()[mastery.get_room_id()] - if room.get_map_id() != map_id: - continue - - var locationListener = ap.SCRIPT_locationListener.new() - locationListener.location_id = mastery.get_ap_id() - locationListener.name = "locationListener_%d" % mastery.get_ap_id() - locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path())) - - get_parent().add_child.call_deferred(locationListener) - - # Set up ending locations. - for ending in gamedata.objects.get_endings(): - var room = gamedata.objects.get_rooms()[ending.get_room_id()] - if room.get_map_id() != map_id: - continue - - var locationListener = ap.SCRIPT_locationListener.new() - locationListener.location_id = ending.get_ap_id() - locationListener.name = "locationListener_%d" % ending.get_ap_id() - locationListener.senders.append(NodePath("/root/scene/" + ending.get_path())) - - get_parent().add_child.call_deferred(locationListener) - - if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name(): - var victoryListener = ap.SCRIPT_victoryListener.new() - victoryListener.name = "victoryListener" - victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path())) - - get_parent().add_child.call_deferred(victoryListener) - - # Set up keyholder locations, in keyholder sanity. - if ap.keyholder_sanity: - for keyholder in gamedata.objects.get_keyholders(): - if not keyholder.has_key(): - continue - - var room = gamedata.objects.get_rooms()[keyholder.get_room_id()] - if room.get_map_id() != map_id: - continue - - var locationListener = ap.SCRIPT_locationListener.new() - locationListener.location_id = keyholder.get_ap_id() - locationListener.name = "locationListener_%d" % keyholder.get_ap_id() - - var khl = khl_script.new() - khl.name = "location_%d_keyholder" % keyholder.get_ap_id() - khl.answer = keyholder.get_key() - khl.senders.append(NodePath("/root/scene/" + keyholder.get_path())) - get_parent().add_child.call_deferred(khl) - - locationListener.senders.append(NodePath("../" + khl.name)) - - get_parent().add_child.call_deferred(locationListener) - # 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) @@ -210,6 +75,45 @@ func _ready(): sign2.material = load("res://assets/materials/%s.material" % sign2_color) get_parent().add_child.call_deferred(sign2) + # Add the gift map entry panel if needed. + if not ap.enable_gift_maps.is_empty(): + var panel_prefab = preload("res://objects/nodes/panel.tscn") + var wpl_prefab = preload("res://objects/nodes/listeners/worldportListener.tscn") + + var giftmap_parent = Node.new() + giftmap_parent.name = "GiftMapEntrance" + get_node("/root/scene/Components").add_child.call_deferred(giftmap_parent) + + var symbolless_player = "" + for i in range(ap.client.ap_user.length()): + if "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".contains( + ap.client.ap_user[i] + ): + symbolless_player = symbolless_player + ap.client.ap_user[i].to_lower() + + var giftmap_panel = panel_prefab.instantiate() + giftmap_panel.name = "Panel" + giftmap_panel.position = Vector3(33.5, 1, 5.5) + giftmap_panel.rotation_degrees = Vector3(-45, 0, 0) + giftmap_panel.clue = "player" + giftmap_panel.answer = symbolless_player + + if ap.enable_gift_maps.has("The Advanced"): + var icely_panel = panel_prefab.instantiate() + icely_panel.name = "IcelyPanel" + icely_panel.answer = "icely" + icely_panel.position = Vector3(33.5, -200, 5.5) + giftmap_panel.proxies.append(NodePath("../IcelyPanel")) + giftmap_parent.add_child.call_deferred(icely_panel) + + var icely_wpl = wpl_prefab.instantiate() + icely_wpl.name = "IcelyWpl" + icely_wpl.exit = "the_advanced" + icely_wpl.senders.append(NodePath("../IcelyPanel")) + giftmap_parent.add_child.call_deferred(icely_wpl) + + giftmap_parent.add_child.call_deferred(giftmap_panel) + # Add the strict purple ending validation. if global.map == "the_sun_temple" and ap.strict_purple_ending: var panel_prefab = preload("res://objects/nodes/panel.tscn") @@ -318,8 +222,172 @@ func _ready(): var rte_trigger = get_node("/root/scene/Components/Warps/triggerArea") rte_trigger.position.z = 0 + # Add the mastery to The Advanced. + if global.map == "the_advanced": + 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 mastery = collectable_prefab.instantiate() + mastery.name = "collectable" + mastery.position = Vector3(0, -200, -5) + mastery.unlock_type = "smiley" + mastery.material_override = load("res://assets/materials/gold.material") + get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery) + + var tpl = tpl_prefab.instantiate() + tpl.teleport_point = Vector3(0, 2, -5) + tpl.teleport_rotate = Vector3(0, 0, 0) + tpl.target_path = mastery + tpl.name = "Teleport" + tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_29")) + tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_30")) + tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_31")) + mastery.add_child.call_deferred(tpl) + + var saver = saver_prefab.instantiate() + saver.name = "saver_collectables" + saver.type = "collectables" + saver.senderGroup.append(NodePath("/root/scene/Components/Collectables")) + get_node("/root/scene").add_child.call_deferred(saver) + ap.update_job_well_done_sign() + # Set up door locations. + var map_id = gamedata.map_id_by_name.get(global.map) + for door in gamedata.objects.get_doors(): + if door.get_map_id() != map_id: + continue + + if not door.has_ap_id(): + continue + + if ( + door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY + or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING + or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR + ): + continue + + var locationListener = ap.SCRIPT_locationListener.new() + locationListener.location_id = door.get_ap_id() + locationListener.name = "locationListener_%d" % door.get_ap_id() + + for panel_ref in door.get_panels(): + var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()] + var panel_path = panel_data.get_path() + + if panel_ref.has_answer(): + for proxy in panel_data.get_proxies(): + if proxy.get_answer() == panel_ref.get_answer(): + panel_path = proxy.get_path() + break + + locationListener.senders.append(NodePath("/root/scene/" + panel_path)) + + for keyholder_ref in door.get_keyholders(): + var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()] + + var khl = khl_script.new() + khl.name = ( + "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()] + ) + khl.answer = keyholder_ref.get_key() + khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path())) + get_parent().add_child.call_deferred(khl) + + locationListener.senders.append(NodePath("../" + khl.name)) + + for sender in door.get_senders(): + locationListener.senders.append(NodePath("/root/scene/" + sender)) + + if door.has_complete_at(): + locationListener.complete_at = door.get_complete_at() + + get_parent().add_child.call_deferred(locationListener) + + # Set up letter locations. + for letter in gamedata.objects.get_letters(): + var room = gamedata.objects.get_rooms()[letter.get_room_id()] + if room.get_map_id() != map_id: + continue + + var locationListener = ap.SCRIPT_locationListener.new() + locationListener.location_id = letter.get_ap_id() + locationListener.name = "locationListener_%d" % letter.get_ap_id() + locationListener.senders.append(NodePath("/root/scene/" + letter.get_path())) + + get_parent().add_child.call_deferred(locationListener) + + if ( + ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2()) + != ap.kLETTER_BEHAVIOR_VANILLA + ): + var scout = ap.scout_location(letter.get_ap_id()) + if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0): + var collectable = get_tree().get_root().get_node("scene").get_node_or_null( + letter.get_path() + ) + if collectable != null: + collectable.setScoutedText.call_deferred(scout["item"]) + + # Set up mastery locations. + for mastery in gamedata.objects.get_masteries(): + var room = gamedata.objects.get_rooms()[mastery.get_room_id()] + if room.get_map_id() != map_id: + continue + + var locationListener = ap.SCRIPT_locationListener.new() + locationListener.location_id = mastery.get_ap_id() + locationListener.name = "locationListener_%d" % mastery.get_ap_id() + locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path())) + + get_parent().add_child.call_deferred(locationListener) + + # Set up ending locations. + for ending in gamedata.objects.get_endings(): + var room = gamedata.objects.get_rooms()[ending.get_room_id()] + if room.get_map_id() != map_id: + continue + + var locationListener = ap.SCRIPT_locationListener.new() + locationListener.location_id = ending.get_ap_id() + locationListener.name = "locationListener_%d" % ending.get_ap_id() + locationListener.senders.append(NodePath("/root/scene/" + ending.get_path())) + + get_parent().add_child.call_deferred(locationListener) + + if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name(): + var victoryListener = ap.SCRIPT_victoryListener.new() + victoryListener.name = "victoryListener" + victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path())) + + get_parent().add_child.call_deferred(victoryListener) + + # Set up keyholder locations, in keyholder sanity. + if ap.keyholder_sanity: + for keyholder in gamedata.objects.get_keyholders(): + if not keyholder.has_key(): + continue + + var room = gamedata.objects.get_rooms()[keyholder.get_room_id()] + if room.get_map_id() != map_id: + continue + + var locationListener = ap.SCRIPT_locationListener.new() + locationListener.location_id = keyholder.get_ap_id() + locationListener.name = "locationListener_%d" % keyholder.get_ap_id() + + var khl = khl_script.new() + khl.name = "location_%d_keyholder" % keyholder.get_ap_id() + khl.answer = keyholder.get_key() + khl.senders.append(NodePath("/root/scene/" + keyholder.get_path())) + get_parent().add_child.call_deferred(khl) + + locationListener.senders.append(NodePath("../" + khl.name)) + + get_parent().add_child.call_deferred(locationListener) + var minimap = ap.SCRIPT_minimap.new() minimap.name = "Minimap" minimap.visible = ap.show_minimap diff --git a/apworld/options.py b/apworld/options.py index 600df6a..4d9c3aa 100644 --- a/apworld/options.py +++ b/apworld/options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range +from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet class ShuffleDoors(DefaultOnToggle): @@ -99,6 +99,23 @@ class EnableIcarus(Toggle): display_name = "Enable Icarus" +class EnableGiftMaps(OptionSet): + """ + Controls whether the beta tester gift maps are randomized. By default, these are not accessible at all from within + the randomizer. Enabling at least one gift map will cause a panel to appear in The Entry's Starting Room. Gift maps + can be accessed by taking a player name that would ordinarily cause the game to load into a gift map, and entering + it into this panel. + + In the base game, nothing happens once you complete a gift map. Masteries have been added to the gift maps in the + randomizer so that the player can be rewarded for completing them. + + Note that the gift maps are intended only for specific people, and as a result may be frustrating or require + knowledge of inside jokes. The Crystalline is particularly difficult as it requires completing a parkour course. + """ + display_name = "Enable Gift Maps" + valid_keys = ["The Advanced", "The Charismatic", "The Crystalline", "The Fuzzy", "The Stellar"] + + class DaedalusRoofAccess(Toggle): """ If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus @@ -178,6 +195,7 @@ class Lingo2Options(PerGameCommonOptions): keyholder_sanity: KeyholderSanity cyan_door_behavior: CyanDoorBehavior enable_icarus: EnableIcarus + enable_gift_maps: EnableGiftMaps daedalus_roof_access: DaedalusRoofAccess strict_purple_ending: StrictPurpleEnding strict_cyan_ending: StrictCyanEnding diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 0cf0473..4f825b9 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -234,6 +234,9 @@ class Lingo2PlayerLogic: return True elif game_map.type == data_pb2.MapType.ICARUS: return bool(world.options.enable_icarus) + elif game_map.type == data_pb2.MapType.GIFT_MAP: + if game_map.name == "the_advanced": + return "The Advanced" in world.options.enable_gift_maps.value return False -- cgit 1.4.1