about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
Diffstat (limited to 'apworld')
-rw-r--r--apworld/__init__.py1
-rw-r--r--apworld/client/client.gd17
-rw-r--r--apworld/client/door.gd31
-rw-r--r--apworld/client/main.gd1
-rw-r--r--apworld/client/manager.gd19
-rw-r--r--apworld/client/paintingAuto.gd43
-rw-r--r--apworld/client/player.gd456
-rw-r--r--apworld/context.py47
-rw-r--r--apworld/options.py31
-rw-r--r--apworld/player_logic.py7
10 files changed, 510 insertions, 143 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):
130 slot_options = [ 130 slot_options = [
131 "cyan_door_behavior", 131 "cyan_door_behavior",
132 "daedalus_roof_access", 132 "daedalus_roof_access",
133 "enable_gift_maps",
133 "enable_icarus", 134 "enable_icarus",
134 "keyholder_sanity", 135 "keyholder_sanity",
135 "shuffle_control_center_colors", 136 "shuffle_control_center_colors",
diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 9a4b402..ce5ac7e 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd
@@ -25,6 +25,7 @@ var _slot_data = {}
25var _accessible_locations = [] 25var _accessible_locations = []
26var _accessible_worldports = [] 26var _accessible_worldports = []
27var _goal_accessible = false 27var _goal_accessible = false
28var _latched_doors = []
28 29
29signal could_not_connect 30signal could_not_connect
30signal connect_status 31signal connect_status
@@ -34,6 +35,7 @@ signal location_scout_received(location_id, item_name, player_name, flags, for_s
34signal text_message_received(message) 35signal text_message_received(message)
35signal item_sent_notification(message) 36signal item_sent_notification(message)
36signal hint_received(message) 37signal hint_received(message)
38signal door_latched(id)
37signal accessible_locations_updated 39signal accessible_locations_updated
38signal checked_locations_updated 40signal checked_locations_updated
39signal checked_worldports_updated 41signal checked_worldports_updated
@@ -189,6 +191,14 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo
189 message["type"], int(message.get("id", null)), message["path"] 191 message["type"], int(message.get("id", null)), message["path"]
190 ) 192 )
191 193
194 elif cmd == "UpdateLatches":
195 for id in message["latches"]:
196 var iid = int(id)
197 if not _latched_doors.has(iid):
198 _latched_doors.append(iid)
199
200 door_latched.emit(iid)
201
192 202
193func connectToServer(server, un, pw): 203func connectToServer(server, un, pw):
194 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) 204 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
@@ -255,6 +265,13 @@ func checkWorldport(port_id):
255 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}]) 265 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}])
256 266
257 267
268func latchDoor(id):
269 if not _latched_doors.has(id):
270 _latched_doors.append(id)
271
272 sendMessage([{"cmd": "LatchDoor", "door": id}])
273
274
258func getLogicalPath(object_type, object_id): 275func getLogicalPath(object_type, object_id):
259 var msg = {"cmd": "GetPath", "type": object_type} 276 var msg = {"cmd": "GetPath", "type": object_type}
260 if object_id != null: 277 if object_id != null:
diff --git a/apworld/client/door.gd b/apworld/client/door.gd index 49f5728..63cfa99 100644 --- a/apworld/client/door.gd +++ b/apworld/client/door.gd
@@ -1,7 +1,9 @@
1extends "res://scripts/nodes/door.gd" 1extends "res://scripts/nodes/door.gd"
2 2
3var door_id
3var item_id 4var item_id
4var item_amount 5var item_amount
6var latched = false
5 7
6 8
7func _ready(): 9func _ready():
@@ -10,7 +12,7 @@ func _ready():
10 ) 12 )
11 13
12 var gamedata = global.get_node("Gamedata") 14 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 15 door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null: 16 if door_id != null:
15 var ap = global.get_node("Archipelago") 17 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id) 18 var item_lock = ap.get_item_id_for_door(door_id)
@@ -27,6 +29,12 @@ func _ready():
27 self.excludeSenders = [] 29 self.excludeSenders = []
28 30
29 call_deferred("_readier") 31 call_deferred("_readier")
32 else:
33 var door_data = gamedata.objects.get_doors()[door_id]
34 if door_data.has_latch() and door_data.get_latch():
35 _check_latched.call_deferred(door_id)
36
37 latched = true
30 38
31 if global.map == "the_sun_temple": 39 if global.map == "the_sun_temple":
32 if name == "spe_EndPlatform" or name == "spe_entry_2": 40 if name == "spe_EndPlatform" or name == "spe_entry_2":
@@ -44,3 +52,24 @@ func _readier():
44 52
45 if ap.client.getItemAmount(item_id) >= item_amount: 53 if ap.client.getItemAmount(item_id) >= item_amount:
46 handleTriggered() 54 handleTriggered()
55
56
57func _check_latched(door_id):
58 var ap = global.get_node("Archipelago")
59
60 if ap.client._latched_doors.has(door_id):
61 triggered = total
62 handleTriggered()
63
64
65func handleTriggered():
66 super.handleTriggered()
67
68 if latched and ran:
69 var ap = global.get_node("Archipelago")
70 ap.client.latchDoor(door_id)
71
72
73func handleUntriggered():
74 if not latched or not ran:
75 super.handleUntriggered()
diff --git a/apworld/client/main.gd b/apworld/client/main.gd index e1f9610..3a62f81 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd
@@ -43,6 +43,7 @@ func _ready():
43 installScriptExtension(runtime.load_script("keyHolderChecker.gd")) 43 installScriptExtension(runtime.load_script("keyHolderChecker.gd"))
44 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd")) 44 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd"))
45 installScriptExtension(runtime.load_script("painting.gd")) 45 installScriptExtension(runtime.load_script("painting.gd"))
46 installScriptExtension(runtime.load_script("paintingAuto.gd"))
46 installScriptExtension(runtime.load_script("panel.gd")) 47 installScriptExtension(runtime.load_script("panel.gd"))
47 installScriptExtension(runtime.load_script("pauseMenu.gd")) 48 installScriptExtension(runtime.load_script("pauseMenu.gd"))
48 installScriptExtension(runtime.load_script("player.gd")) 49 installScriptExtension(runtime.load_script("player.gd"))
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index dac09b2..41ab648 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd
@@ -63,6 +63,8 @@ const kEndingNameByVictoryValue = {
63var apworld_version = [0, 0, 0] 63var apworld_version = [0, 0, 0]
64var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 64var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
65var daedalus_roof_access = false 65var daedalus_roof_access = false
66var enable_gift_maps = []
67var enable_icarus = false
66var keyholder_sanity = false 68var keyholder_sanity = false
67var port_pairings = {} 69var port_pairings = {}
68var shuffle_control_center_colors = false 70var shuffle_control_center_colors = false
@@ -141,6 +143,7 @@ func _ready():
141 client.accessible_locations_updated.connect(_on_accessible_locations_updated) 143 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
142 client.checked_locations_updated.connect(_on_checked_locations_updated) 144 client.checked_locations_updated.connect(_on_checked_locations_updated)
143 client.checked_worldports_updated.connect(_on_checked_worldports_updated) 145 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
146 client.door_latched.connect(_on_door_latched)
144 147
145 client.could_not_connect.connect(_client_could_not_connect) 148 client.could_not_connect.connect(_client_could_not_connect)
146 client.connect_status.connect(_client_connect_status) 149 client.connect_status.connect(_client_connect_status)
@@ -376,6 +379,20 @@ func _on_checked_worldports_updated():
376 textclient_node.update_worldports() 379 textclient_node.update_worldports()
377 380
378 381
382func _on_door_latched(door_id):
383 var gamedata = global.get_node("Gamedata")
384 if gamedata.get_door_map_name(door_id) != global.map:
385 return
386
387 var receivers = gamedata.get_door_receivers(door_id)
388 var scene = get_tree().get_root().get_node_or_null("scene")
389 if scene != null:
390 for receiver in receivers:
391 var rnode = scene.get_node_or_null(receiver)
392 if rnode != null:
393 rnode.handleTriggered()
394
395
379func _client_could_not_connect(message): 396func _client_could_not_connect(message):
380 could_not_connect.emit(message) 397 could_not_connect.emit(message)
381 398
@@ -424,6 +441,8 @@ func _client_connected(slot_data):
424 # Read slot data. 441 # Read slot data.
425 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) 442 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
426 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) 443 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
444 enable_gift_maps = slot_data.get("enable_gift_maps", [])
445 enable_icarus = bool(slot_data.get("enable_icarus", false))
427 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) 446 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
428 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) 447 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
429 shuffle_doors = bool(slot_data.get("shuffle_doors", false)) 448 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
diff --git a/apworld/client/paintingAuto.gd b/apworld/client/paintingAuto.gd new file mode 100644 index 0000000..553c2c9 --- /dev/null +++ b/apworld/client/paintingAuto.gd
@@ -0,0 +1,43 @@
1extends "res://scripts/nodes/paintingAuto.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 call_deferred("_readier")
30
31 super._ready()
32
33 if item_id != null and activate_on_sender_complete:
34 enabled = false
35 if not hide_particles:
36 get_node("Hinge/paintingColliders/TeleportParticles").emitting = false
37
38
39func _readier():
40 var ap = global.get_node("Archipelago")
41
42 if ap.client.getItemAmount(item_id) >= item_amount:
43 handleTriggered()
diff --git a/apworld/client/player.gd b/apworld/client/player.gd index b73f61e..8aee21a 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd
@@ -19,141 +19,6 @@ func _ready():
19 19
20 ap.start_batching_locations() 20 ap.start_batching_locations()
21 21
22 # Set up door locations.
23 var map_id = gamedata.map_id_by_name.get(global.map)
24 for door in gamedata.objects.get_doors():
25 if door.get_map_id() != map_id:
26 continue
27
28 if not door.has_ap_id():
29 continue
30
31 if (
32 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
33 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
34 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
35 ):
36 continue
37
38 var locationListener = ap.SCRIPT_locationListener.new()
39 locationListener.location_id = door.get_ap_id()
40 locationListener.name = "locationListener_%d" % door.get_ap_id()
41
42 for panel_ref in door.get_panels():
43 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
44 var panel_path = panel_data.get_path()
45
46 if panel_ref.has_answer():
47 for proxy in panel_data.get_proxies():
48 if proxy.get_answer() == panel_ref.get_answer():
49 panel_path = proxy.get_path()
50 break
51
52 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
53
54 for keyholder_ref in door.get_keyholders():
55 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
56
57 var khl = khl_script.new()
58 khl.name = (
59 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
60 )
61 khl.answer = keyholder_ref.get_key()
62 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
63 get_parent().add_child.call_deferred(khl)
64
65 locationListener.senders.append(NodePath("../" + khl.name))
66
67 for sender in door.get_senders():
68 locationListener.senders.append(NodePath("/root/scene/" + sender))
69
70 if door.has_complete_at():
71 locationListener.complete_at = door.get_complete_at()
72
73 get_parent().add_child.call_deferred(locationListener)
74
75 # Set up letter locations.
76 for letter in gamedata.objects.get_letters():
77 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
78 if room.get_map_id() != map_id:
79 continue
80
81 var locationListener = ap.SCRIPT_locationListener.new()
82 locationListener.location_id = letter.get_ap_id()
83 locationListener.name = "locationListener_%d" % letter.get_ap_id()
84 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
85
86 get_parent().add_child.call_deferred(locationListener)
87
88 if (
89 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
90 != ap.kLETTER_BEHAVIOR_VANILLA
91 ):
92 var scout = ap.scout_location(letter.get_ap_id())
93 if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0):
94 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
95 letter.get_path()
96 )
97 if collectable != null:
98 collectable.setScoutedText.call_deferred(scout["item"])
99
100 # Set up mastery locations.
101 for mastery in gamedata.objects.get_masteries():
102 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
103 if room.get_map_id() != map_id:
104 continue
105
106 var locationListener = ap.SCRIPT_locationListener.new()
107 locationListener.location_id = mastery.get_ap_id()
108 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
109 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
110
111 get_parent().add_child.call_deferred(locationListener)
112
113 # Set up ending locations.
114 for ending in gamedata.objects.get_endings():
115 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
116 if room.get_map_id() != map_id:
117 continue
118
119 var locationListener = ap.SCRIPT_locationListener.new()
120 locationListener.location_id = ending.get_ap_id()
121 locationListener.name = "locationListener_%d" % ending.get_ap_id()
122 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
123
124 get_parent().add_child.call_deferred(locationListener)
125
126 if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
127 var victoryListener = ap.SCRIPT_victoryListener.new()
128 victoryListener.name = "victoryListener"
129 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
130
131 get_parent().add_child.call_deferred(victoryListener)
132
133 # Set up keyholder locations, in keyholder sanity.
134 if ap.keyholder_sanity:
135 for keyholder in gamedata.objects.get_keyholders():
136 if not keyholder.has_key():
137 continue
138
139 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
140 if room.get_map_id() != map_id:
141 continue
142
143 var locationListener = ap.SCRIPT_locationListener.new()
144 locationListener.location_id = keyholder.get_ap_id()
145 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
146
147 var khl = khl_script.new()
148 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
149 khl.answer = keyholder.get_key()
150 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
151 get_parent().add_child.call_deferred(khl)
152
153 locationListener.senders.append(NodePath("../" + khl.name))
154
155 get_parent().add_child.call_deferred(locationListener)
156
157 # Block off roof access in Daedalus. 22 # Block off roof access in Daedalus.
158 if global.map == "daedalus" and not ap.daedalus_roof_access: 23 if global.map == "daedalus" and not ap.daedalus_roof_access:
159 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49) 24 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
@@ -210,6 +75,84 @@ func _ready():
210 sign2.material = load("res://assets/materials/%s.material" % sign2_color) 75 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
211 get_parent().add_child.call_deferred(sign2) 76 get_parent().add_child.call_deferred(sign2)
212 77
78 # Add the gift map entry panel if needed.
79 if not ap.enable_gift_maps.is_empty():
80 var panel_prefab = preload("res://objects/nodes/panel.tscn")
81 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
82 var wpl_prefab = preload("res://objects/nodes/listeners/worldportListener.tscn")
83
84 var giftmap_parent = Node.new()
85 giftmap_parent.name = "GiftMapEntrance"
86 get_node("/root/scene/Components").add_child.call_deferred(giftmap_parent)
87
88 var symbolless_player = ""
89 for i in range(ap.client.ap_user.length()):
90 if "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(
91 ap.client.ap_user[i]
92 ):
93 symbolless_player = symbolless_player + ap.client.ap_user[i].to_lower()
94
95 var giftmap_panel = panel_prefab.instantiate()
96 giftmap_panel.name = "Panel"
97 giftmap_panel.position = Vector3(33.5, -190, 5.5)
98 giftmap_panel.rotation_degrees = Vector3(-45, 0, 0)
99 giftmap_panel.clue = "player"
100 giftmap_panel.answer = symbolless_player
101
102 if ap.enable_gift_maps.has("The Advanced"):
103 var icely_panel = panel_prefab.instantiate()
104 icely_panel.name = "IcelyPanel"
105 icely_panel.answer = "icely"
106 icely_panel.position = Vector3(33.5, -200, 5.5)
107 giftmap_panel.proxies.append(NodePath("../IcelyPanel"))
108 giftmap_parent.add_child.call_deferred(icely_panel)
109
110 var icely_wpl = wpl_prefab.instantiate()
111 icely_wpl.name = "IcelyWpl"
112 icely_wpl.exit = "the_advanced"
113 icely_wpl.senders.append(NodePath("../IcelyPanel"))
114 giftmap_parent.add_child.call_deferred(icely_wpl)
115
116 if ap.enable_gift_maps.has("The Charismatic"):
117 var souvey_panel = panel_prefab.instantiate()
118 souvey_panel.name = "SouveyPanel"
119 souvey_panel.answer = "souvey"
120 souvey_panel.position = Vector3(33.5, -210, 5.5)
121 giftmap_panel.proxies.append(NodePath("../SouveyPanel"))
122 giftmap_parent.add_child.call_deferred(souvey_panel)
123
124 var souvey_wpl = wpl_prefab.instantiate()
125 souvey_wpl.name = "SouveyWpl"
126 souvey_wpl.exit = "the_charismatic"
127 souvey_wpl.senders.append(NodePath("../SouveyPanel"))
128 giftmap_parent.add_child.call_deferred(souvey_wpl)
129
130 if ap.enable_gift_maps.has("The Crystalline"):
131 var q_panel = panel_prefab.instantiate()
132 q_panel.name = "QPanel"
133 q_panel.answer = "q"
134 q_panel.position = Vector3(33.5, -220, 5.5)
135 giftmap_panel.proxies.append(NodePath("../QPanel"))
136 giftmap_parent.add_child.call_deferred(q_panel)
137
138 var q_wpl = wpl_prefab.instantiate()
139 q_wpl.name = "QWpl"
140 q_wpl.exit = "the_crystalline"
141 q_wpl.senders.append(NodePath("../QPanel"))
142 giftmap_parent.add_child.call_deferred(q_wpl)
143
144 giftmap_parent.add_child.call_deferred(giftmap_panel)
145
146 var giftmap_tpl = tpl_prefab.instantiate()
147 giftmap_tpl.name = "PanelTeleporter"
148 giftmap_tpl.teleport_point = Vector3(33.5, 1, 5.5)
149 giftmap_tpl.teleport_rotate = Vector3(-45, 0, 0)
150 giftmap_tpl.target_path = giftmap_panel
151 giftmap_tpl.senders.append(
152 NodePath("/root/scene/Components/Listeners/unlockReaderListenerDoubles")
153 )
154 giftmap_parent.add_child.call_deferred(giftmap_tpl)
155
213 # Add the strict purple ending validation. 156 # Add the strict purple ending validation.
214 if global.map == "the_sun_temple" and ap.strict_purple_ending: 157 if global.map == "the_sun_temple" and ap.strict_purple_ending:
215 var panel_prefab = preload("res://objects/nodes/panel.tscn") 158 var panel_prefab = preload("res://objects/nodes/panel.tscn")
@@ -313,8 +256,251 @@ func _ready():
313 entry1.get_parent().add_child.call_deferred(entry12) 256 entry1.get_parent().add_child.call_deferred(entry12)
314 entry1.queue_free() 257 entry1.queue_free()
315 258
259 # Move the Plaza RTE trigger outside of the turtle.
260 if global.map == "the_plaza":
261 var rte_trigger = get_node("/root/scene/Components/Warps/triggerArea")
262 rte_trigger.position.z = 0
263
264 # Add the mastery to Icarus.
265 if global.map == "icarus" and ap.enable_icarus:
266 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
267 var saver_prefab = preload("res://objects/nodes/saver.tscn")
268 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
269
270 var mastery = collectable_prefab.instantiate()
271 mastery.name = "collectable"
272 mastery.position = Vector3(0, -2000, 0)
273 mastery.unlock_type = "smiley"
274 mastery.material_override = load("res://assets/materials/gold.material")
275 get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
276
277 var tpl = tpl_prefab.instantiate()
278 tpl.teleport_point = Vector3(56.25, 0, -5.5)
279 tpl.teleport_rotate = Vector3(0, 0, 0)
280 tpl.target_path = mastery
281 tpl.name = "Teleport"
282 tpl.senderGroup.append(NodePath("/root/scene/Panels"))
283 tpl.nested = true
284 mastery.add_child.call_deferred(tpl)
285
286 var saver = saver_prefab.instantiate()
287 saver.name = "saver_collectables"
288 saver.type = "collectables"
289 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
290 get_node("/root/scene").add_child.call_deferred(saver)
291
292 # Add the mastery to The Advanced.
293 if global.map == "the_advanced":
294 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
295 var saver_prefab = preload("res://objects/nodes/saver.tscn")
296 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
297
298 var mastery = collectable_prefab.instantiate()
299 mastery.name = "collectable"
300 mastery.position = Vector3(0, -200, -5)
301 mastery.unlock_type = "smiley"
302 mastery.material_override = load("res://assets/materials/gold.material")
303 get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
304
305 var tpl = tpl_prefab.instantiate()
306 tpl.teleport_point = Vector3(0, 2, -5)
307 tpl.teleport_rotate = Vector3(0, 0, 0)
308 tpl.target_path = mastery
309 tpl.name = "Teleport"
310 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_29"))
311 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_30"))
312 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_31"))
313 mastery.add_child.call_deferred(tpl)
314
315 var saver = saver_prefab.instantiate()
316 saver.name = "saver_collectables"
317 saver.type = "collectables"
318 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
319 get_node("/root/scene").add_child.call_deferred(saver)
320
321 # Add the mastery to The Charismatic.
322 if global.map == "the_charismatic":
323 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
324 var saver_prefab = preload("res://objects/nodes/saver.tscn")
325
326 var mastery = collectable_prefab.instantiate()
327 mastery.name = "collectable"
328 mastery.position = Vector3(-17, 2, -29)
329 mastery.rotation_degrees = Vector3(0, 45, 0)
330 mastery.unlock_type = "smiley"
331 mastery.material_override = load("res://assets/materials/gold.material")
332 get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
333
334 var saver = saver_prefab.instantiate()
335 saver.name = "saver_collectables"
336 saver.type = "collectables"
337 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
338 get_node("/root/scene").add_child.call_deferred(saver)
339
340 # Add the mastery to The Crystalline.
341 if global.map == "the_crystalline":
342 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
343 var saver_prefab = preload("res://objects/nodes/saver.tscn")
344 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
345
346 var mastery = collectable_prefab.instantiate()
347 mastery.name = "collectable"
348 mastery.position = Vector3(0, 13, 37)
349 mastery.unlock_type = "smiley"
350 mastery.material_override = load("res://assets/materials/gold.material")
351 get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
352
353 var tpl = tpl_prefab.instantiate()
354 tpl.teleport_point = Vector3(0, 11.5, -20)
355 tpl.teleport_rotate = Vector3(0, 0, 180)
356 tpl.target_path = mastery
357 tpl.name = "Teleport"
358 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_3"))
359 mastery.add_child.call_deferred(tpl)
360
361 var saver = saver_prefab.instantiate()
362 saver.name = "saver_collectables"
363 saver.type = "collectables"
364 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
365 get_node("/root/scene").add_child.call_deferred(saver)
366
316 ap.update_job_well_done_sign() 367 ap.update_job_well_done_sign()
317 368
369 # Set up door locations.
370 var map_id = gamedata.map_id_by_name.get(global.map)
371 for door in gamedata.objects.get_doors():
372 if door.get_map_id() != map_id:
373 continue
374
375 if not door.has_ap_id():
376 continue
377
378 if (
379 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
380 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
381 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
382 ):
383 continue
384
385 var locationListener = ap.SCRIPT_locationListener.new()
386 locationListener.location_id = door.get_ap_id()
387 locationListener.name = "locationListener_%d" % door.get_ap_id()
388
389 for panel_ref in door.get_panels():
390 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
391 var panel_path = panel_data.get_path()
392
393 if panel_ref.has_answer():
394 for proxy in panel_data.get_proxies():
395 if proxy.get_answer() == panel_ref.get_answer():
396 panel_path = proxy.get_path()
397 break
398
399 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
400
401 for keyholder_ref in door.get_keyholders():
402 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
403
404 var khl = khl_script.new()
405 khl.name = (
406 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
407 )
408 khl.answer = keyholder_ref.get_key()
409 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
410 get_parent().add_child.call_deferred(khl)
411
412 locationListener.senders.append(NodePath("../" + khl.name))
413
414 for sender in door.get_senders():
415 locationListener.senders.append(NodePath("/root/scene/" + sender))
416
417 if door.has_complete_at():
418 locationListener.complete_at = door.get_complete_at()
419
420 get_parent().add_child.call_deferred(locationListener)
421
422 # Set up letter locations.
423 for letter in gamedata.objects.get_letters():
424 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
425 if room.get_map_id() != map_id:
426 continue
427
428 var locationListener = ap.SCRIPT_locationListener.new()
429 locationListener.location_id = letter.get_ap_id()
430 locationListener.name = "locationListener_%d" % letter.get_ap_id()
431 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
432
433 get_parent().add_child.call_deferred(locationListener)
434
435 if (
436 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
437 != ap.kLETTER_BEHAVIOR_VANILLA
438 ):
439 var scout = ap.scout_location(letter.get_ap_id())
440 if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0):
441 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
442 letter.get_path()
443 )
444 if collectable != null:
445 collectable.setScoutedText.call_deferred(scout["item"])
446
447 # Set up mastery locations.
448 for mastery in gamedata.objects.get_masteries():
449 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
450 if room.get_map_id() != map_id:
451 continue
452
453 var locationListener = ap.SCRIPT_locationListener.new()
454 locationListener.location_id = mastery.get_ap_id()
455 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
456 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
457
458 get_parent().add_child.call_deferred(locationListener)
459
460 # Set up ending locations.
461 for ending in gamedata.objects.get_endings():
462 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
463 if room.get_map_id() != map_id:
464 continue
465
466 var locationListener = ap.SCRIPT_locationListener.new()
467 locationListener.location_id = ending.get_ap_id()
468 locationListener.name = "locationListener_%d" % ending.get_ap_id()
469 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
470
471 get_parent().add_child.call_deferred(locationListener)
472
473 if ap.kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
474 var victoryListener = ap.SCRIPT_victoryListener.new()
475 victoryListener.name = "victoryListener"
476 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
477
478 get_parent().add_child.call_deferred(victoryListener)
479
480 # Set up keyholder locations, in keyholder sanity.
481 if ap.keyholder_sanity:
482 for keyholder in gamedata.objects.get_keyholders():
483 if not keyholder.has_key():
484 continue
485
486 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
487 if room.get_map_id() != map_id:
488 continue
489
490 var locationListener = ap.SCRIPT_locationListener.new()
491 locationListener.location_id = keyholder.get_ap_id()
492 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
493
494 var khl = khl_script.new()
495 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
496 khl.answer = keyholder.get_key()
497 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
498 get_parent().add_child.call_deferred(khl)
499
500 locationListener.senders.append(NodePath("../" + khl.name))
501
502 get_parent().add_child.call_deferred(locationListener)
503
318 var minimap = ap.SCRIPT_minimap.new() 504 var minimap = ap.SCRIPT_minimap.new()
319 minimap.name = "Minimap" 505 minimap.name = "Minimap"
320 minimap.visible = ap.show_minimap 506 minimap.visible = ap.show_minimap
diff --git a/apworld/context.py b/apworld/context.py index d59bf9d..7975686 100644 --- a/apworld/context.py +++ b/apworld/context.py
@@ -38,6 +38,7 @@ class Lingo2Manager:
38 keyboard: dict[str, int] 38 keyboard: dict[str, int]
39 worldports: set[int] 39 worldports: set[int]
40 goaled: bool 40 goaled: bool
41 latches: set[int]
41 42
42 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): 43 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"):
43 self.game_ctx = game_ctx 44 self.game_ctx = game_ctx
@@ -47,6 +48,7 @@ class Lingo2Manager:
47 self.tracker = Tracker(self) 48 self.tracker = Tracker(self)
48 self.keyboard = {} 49 self.keyboard = {}
49 self.worldports = set() 50 self.worldports = set()
51 self.latches = set()
50 52
51 self.reset() 53 self.reset()
52 54
@@ -56,6 +58,7 @@ class Lingo2Manager:
56 58
57 self.worldports = set() 59 self.worldports = set()
58 self.goaled = False 60 self.goaled = False
61 self.latches = set()
59 62
60 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: 63 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]:
61 ret: dict[str, int] = {} 64 ret: dict[str, int] = {}
@@ -81,6 +84,12 @@ class Lingo2Manager:
81 84
82 return ret 85 return ret
83 86
87 def update_latches(self, new_latches: set[int]) -> set[int]:
88 ret = new_latches.difference(self.latches)
89 self.latches.update(new_latches)
90
91 return ret
92
84 93
85class Lingo2GameContext: 94class Lingo2GameContext:
86 server: Endpoint | None 95 server: Endpoint | None
@@ -244,6 +253,17 @@ class Lingo2GameContext:
244 253
245 async_start(self.send_msgs([msg]), name="path reply") 254 async_start(self.send_msgs([msg]), name="path reply")
246 255
256 def send_update_latches(self, latches):
257 if self.server is None:
258 return
259
260 msg = {
261 "cmd": "UpdateLatches",
262 "latches": latches,
263 }
264
265 async_start(self.send_msgs([msg]), name="update latches")
266
247 async def send_msgs(self, msgs: list[Any]) -> None: 267 async def send_msgs(self, msgs: list[Any]) -> None:
248 """ `msgs` JSON serializable """ 268 """ `msgs` JSON serializable """
249 if not self.server or not self.server.socket.open or self.server.socket.closed: 269 if not self.server or not self.server.socket.open or self.server.socket.closed:
@@ -298,7 +318,7 @@ class Lingo2ClientContext(CommonContext):
298 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" 318 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}"
299 319
300 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), 320 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"),
301 self.victory_data_storage_key) 321 self.victory_data_storage_key, self.get_datastorage_key("latches"))
302 msg_batch = [{ 322 msg_batch = [{
303 "cmd": "Set", 323 "cmd": "Set",
304 "key": self.get_datastorage_key("keyboard1"), 324 "key": self.get_datastorage_key("keyboard1"),
@@ -311,6 +331,12 @@ class Lingo2ClientContext(CommonContext):
311 "default": 0, 331 "default": 0,
312 "want_reply": True, 332 "want_reply": True,
313 "operations": [{"operation": "default", "value": 0}] 333 "operations": [{"operation": "default", "value": 0}]
334 }, {
335 "cmd": "Set",
336 "key": self.get_datastorage_key("latches"),
337 "default": [],
338 "want_reply": True,
339 "operations": [{"operation": "default", "value": []}]
314 }] 340 }]
315 341
316 if self.slot_data.get("shuffle_worldports", False): 342 if self.slot_data.get("shuffle_worldports", False):
@@ -420,6 +446,10 @@ class Lingo2ClientContext(CommonContext):
420 self.manager.game_ctx.send_update_worldports(updates) 446 self.manager.game_ctx.send_update_worldports(updates)
421 elif args["key"] == self.victory_data_storage_key: 447 elif args["key"] == self.victory_data_storage_key:
422 self.handle_status_update(args["value"]) 448 self.handle_status_update(args["value"])
449 elif args["key"] == self.get_datastorage_key("latches"):
450 updates = self.manager.update_latches(set(args["value"]))
451 if len(updates) > 0:
452 self.manager.game_ctx.send_update_latches(updates)
423 453
424 def get_datastorage_key(self, name: str): 454 def get_datastorage_key(self, name: str):
425 return f"Lingo2_{self.slot}_{name}" 455 return f"Lingo2_{self.slot}_{name}"
@@ -501,6 +531,17 @@ class Lingo2ClientContext(CommonContext):
501 self.manager.tracker.refresh_state() 531 self.manager.tracker.refresh_state()
502 self.manager.game_ctx.send_accessible_locations() 532 self.manager.game_ctx.send_accessible_locations()
503 533
534 async def update_latches(self, updates: set[int]):
535 await self.send_msgs([{
536 "cmd": "Set",
537 "key": self.get_datastorage_key("latches"),
538 "want_reply": True,
539 "operations": [{
540 "operation": "update",
541 "value": updates
542 }]
543 }])
544
504 545
505async def pipe_loop(manager: Lingo2Manager): 546async def pipe_loop(manager: Lingo2Manager):
506 while not manager.client_ctx.exit_event.is_set(): 547 while not manager.client_ctx.exit_event.is_set():
@@ -571,6 +612,10 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict):
571 path = manager.tracker.get_path_to_goal() 612 path = manager.tracker.get_path_to_goal()
572 613
573 manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path) 614 manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path)
615 elif cmd == "LatchDoor":
616 updates = manager.update_latches({args["door"]})
617 if len(updates) > 0:
618 async_start(manager.client_ctx.update_latches(updates), name="client update latches")
574 elif cmd == "Quit": 619 elif cmd == "Quit":
575 manager.client_ctx.exit_event.set() 620 manager.client_ctx.exit_event.set()
576 621
diff --git a/apworld/options.py b/apworld/options.py index 5d1fd7c..7577e0c 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -1,6 +1,6 @@
1from dataclasses import dataclass 1from dataclasses import dataclass
2 2
3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range 3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet
4 4
5 5
6class ShuffleDoors(DefaultOnToggle): 6class ShuffleDoors(DefaultOnToggle):
@@ -56,11 +56,7 @@ class ShuffleWorldports(Toggle):
56 """ 56 """
57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in 57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in
58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections 58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections
59 like the one between The Shop and Control Center. Connections that depend on placing letters in keyholders are also 59 like the one between The Shop and Control Center.
60 currently not shuffled.
61
62 NOTE: It is highly recommended that you turn on Shuffle Control Center Colors when using Shuffle Worldports. Not
63 doing so runs the risk of creating an unfinishable seed.
64 """ 60 """
65 display_name = "Shuffle Worldports" 61 display_name = "Shuffle Worldports"
66 62
@@ -103,6 +99,28 @@ class EnableIcarus(Toggle):
103 display_name = "Enable Icarus" 99 display_name = "Enable Icarus"
104 100
105 101
102class EnableGiftMaps(OptionSet):
103 """
104 Controls whether the beta tester gift maps are randomized. By default, these are not accessible at all from within
105 the randomizer. This option allows you to enter the maps, and creates items and locations for them. If worldport
106 shuffle is on, their worldports will be included in the randomization.
107
108 The gift maps are accessed via a panel in The Entry's Starting Room, which only appears if at least one gift map is
109 enabled. It is also treated like a cyan door, and will not appear until the condition specified in the Cyan Door
110 Behavior option is satisfied. Solving this panel with the name of one of the beta testers will teleport you to their
111 corresponding gift map.
112
113 In the base game, nothing happens once you complete a gift map. Masteries have been added to the gift maps in the
114 randomizer so that the player can be rewarded for completing them.
115
116 Note that the gift maps were originally only intended to be played by specific people, and as a result may be
117 frustrating or require knowledge of inside jokes. The Crystalline is particularly difficult as it requires
118 completing a parkour course.
119 """
120 display_name = "Enable Gift Maps"
121 valid_keys = ["The Advanced", "The Charismatic", "The Crystalline", "The Fuzzy", "The Stellar"]
122
123
106class DaedalusRoofAccess(Toggle): 124class DaedalusRoofAccess(Toggle):
107 """ 125 """
108 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 126 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus
@@ -182,6 +200,7 @@ class Lingo2Options(PerGameCommonOptions):
182 keyholder_sanity: KeyholderSanity 200 keyholder_sanity: KeyholderSanity
183 cyan_door_behavior: CyanDoorBehavior 201 cyan_door_behavior: CyanDoorBehavior
184 enable_icarus: EnableIcarus 202 enable_icarus: EnableIcarus
203 enable_gift_maps: EnableGiftMaps
185 daedalus_roof_access: DaedalusRoofAccess 204 daedalus_roof_access: DaedalusRoofAccess
186 strict_purple_ending: StrictPurpleEnding 205 strict_purple_ending: StrictPurpleEnding
187 strict_cyan_ending: StrictCyanEnding 206 strict_cyan_ending: StrictCyanEnding
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 0cf0473..5f4f1d7 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -234,6 +234,13 @@ class Lingo2PlayerLogic:
234 return True 234 return True
235 elif game_map.type == data_pb2.MapType.ICARUS: 235 elif game_map.type == data_pb2.MapType.ICARUS:
236 return bool(world.options.enable_icarus) 236 return bool(world.options.enable_icarus)
237 elif game_map.type == data_pb2.MapType.GIFT_MAP:
238 if game_map.name == "the_advanced":
239 return "The Advanced" in world.options.enable_gift_maps.value
240 elif game_map.name == "the_charismatic":
241 return "The Charismatic" in world.options.enable_gift_maps.value
242 elif game_map.name == "the_crystalline":
243 return "The Crystalline" in world.options.enable_gift_maps.value
237 244
238 return False 245 return False
239 246