about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--apworld/__init__.py14
-rw-r--r--apworld/docs/en_Lingo_2.md4
-rw-r--r--apworld/items.py24
-rw-r--r--apworld/options.py9
-rw-r--r--apworld/player_logic.py60
-rw-r--r--apworld/requirements.txt2
-rw-r--r--apworld/rules.py2
-rw-r--r--apworld/static_logic.py4
-rw-r--r--client/Archipelago/gamedata.gd47
-rw-r--r--client/Archipelago/manager.gd10
-rw-r--r--client/Archipelago/panel.gd101
-rw-r--r--client/Archipelago/player.gd7
-rw-r--r--client/Archipelago/settings_screen.gd4
-rw-r--r--data/connections.txtpb20
-rw-r--r--data/ids.yaml19
-rw-r--r--data/maps/the_entry/rooms/Starting Room.txtpb4
-rw-r--r--data/maps/the_great/doors.txtpb5
-rw-r--r--data/maps/the_great/rooms/West Side.txtpb1
-rw-r--r--data/maps/the_owl/doors.txtpb2
-rw-r--r--data/metadata.txtpb22
-rw-r--r--proto/human.proto4
-rw-r--r--tools/assign_ids/main.cpp18
22 files changed, 357 insertions, 26 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index d05bd38..8051e0f 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -1,7 +1,7 @@
1""" 1"""
2Archipelago init file for Lingo 2 2Archipelago init file for Lingo 2
3""" 3"""
4from BaseClasses import ItemClassification, Item 4from BaseClasses import ItemClassification, Item, Tutorial
5from worlds.AutoWorld import WebWorld, World 5from worlds.AutoWorld import WebWorld, World
6from .items import Lingo2Item 6from .items import Lingo2Item
7from .options import Lingo2Options 7from .options import Lingo2Options
@@ -9,10 +9,20 @@ from .player_logic import Lingo2PlayerLogic
9from .regions import create_regions 9from .regions import create_regions
10from .static_logic import Lingo2StaticLogic 10from .static_logic import Lingo2StaticLogic
11 11
12MAJOR_VERSION = 1
13MINOR_VERSION = 0
12 14
13class Lingo2WebWorld(WebWorld): 15class Lingo2WebWorld(WebWorld):
14 rich_text_options_doc = True 16 rich_text_options_doc = True
15 theme = "grass" 17 theme = "grass"
18 tutorials = [Tutorial(
19 "Multiworld Setup Guide",
20 "A guide to playing Lingo 2 with Archipelago.",
21 "English",
22 "en_Lingo_2.md",
23 "setup/en",
24 ["hatkirby"]
25 )]
16 26
17 27
18class Lingo2World(World): 28class Lingo2World(World):
@@ -74,11 +84,13 @@ class Lingo2World(World):
74 "shuffle_control_center_colors", 84 "shuffle_control_center_colors",
75 "shuffle_doors", 85 "shuffle_doors",
76 "shuffle_letters", 86 "shuffle_letters",
87 "shuffle_symbols",
77 "victory_condition", 88 "victory_condition",
78 ] 89 ]
79 90
80 slot_data = { 91 slot_data = {
81 **self.options.as_dict(*slot_options), 92 **self.options.as_dict(*slot_options),
93 "version": [MAJOR_VERSION, MINOR_VERSION],
82 } 94 }
83 95
84 return slot_data 96 return slot_data
diff --git a/apworld/docs/en_Lingo_2.md b/apworld/docs/en_Lingo_2.md new file mode 100644 index 0000000..977795a --- /dev/null +++ b/apworld/docs/en_Lingo_2.md
@@ -0,0 +1,4 @@
1# Lingo 2
2
3See [the project README](https://code.fourisland.com/lingo2-archipelago/about/)
4for installation instructions and frequently asked questions. \ No newline at end of file
diff --git a/apworld/items.py b/apworld/items.py index 971a709..32568a3 100644 --- a/apworld/items.py +++ b/apworld/items.py
@@ -1,5 +1,29 @@
1from .generated import data_pb2 as data_pb2
1from BaseClasses import Item 2from BaseClasses import Item
2 3
3 4
4class Lingo2Item(Item): 5class Lingo2Item(Item):
5 game: str = "Lingo 2" 6 game: str = "Lingo 2"
7
8
9SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = {
10 data_pb2.PuzzleSymbol.SUN: "Sun Symbol",
11 data_pb2.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
12 data_pb2.PuzzleSymbol.ZERO: "Zero Symbol",
13 data_pb2.PuzzleSymbol.EXAMPLE: "Example Symbol",
14 data_pb2.PuzzleSymbol.BOXES: "Boxes Symbol",
15 data_pb2.PuzzleSymbol.PLANET: "Planet Symbol",
16 data_pb2.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
17 data_pb2.PuzzleSymbol.CROSS: "Cross Symbol",
18 data_pb2.PuzzleSymbol.SWEET: "Sweet Symbol",
19 data_pb2.PuzzleSymbol.GENDER: "Gender Symbol",
20 data_pb2.PuzzleSymbol.AGE: "Age Symbol",
21 data_pb2.PuzzleSymbol.SOUND: "Sound Symbol",
22 data_pb2.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
23 data_pb2.PuzzleSymbol.JOB: "Job Symbol",
24 data_pb2.PuzzleSymbol.STARS: "Stars Symbol",
25 data_pb2.PuzzleSymbol.NULL: "Null Symbol",
26 data_pb2.PuzzleSymbol.EVAL: "Eval Symbol",
27 data_pb2.PuzzleSymbol.LINGO: "Lingo Symbol",
28 data_pb2.PuzzleSymbol.QUESTION: "Question Symbol",
29}
diff --git a/apworld/options.py b/apworld/options.py index 2197b0f..f72e826 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -39,6 +39,14 @@ class ShuffleLetters(Choice):
39 option_item_cyan = 4 39 option_item_cyan = 4
40 40
41 41
42class ShuffleSymbols(Toggle):
43 """
44 If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel.
45 Players will be prevented from solving puzzles with symbols on them until all of the required symbols are unlocked.
46 """
47 display_name = "Shuffle Symbols"
48
49
42class KeyholderSanity(Toggle): 50class KeyholderSanity(Toggle):
43 """ 51 """
44 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder. 52 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder.
@@ -102,6 +110,7 @@ class Lingo2Options(PerGameCommonOptions):
102 shuffle_doors: ShuffleDoors 110 shuffle_doors: ShuffleDoors
103 shuffle_control_center_colors: ShuffleControlCenterColors 111 shuffle_control_center_colors: ShuffleControlCenterColors
104 shuffle_letters: ShuffleLetters 112 shuffle_letters: ShuffleLetters
113 shuffle_symbols: ShuffleSymbols
105 keyholder_sanity: KeyholderSanity 114 keyholder_sanity: KeyholderSanity
106 cyan_door_behavior: CyanDoorBehavior 115 cyan_door_behavior: CyanDoorBehavior
107 daedalus_roof_access: DaedalusRoofAccess 116 daedalus_roof_access: DaedalusRoofAccess
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index c94b809..42b36e6 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -1,6 +1,7 @@
1from enum import IntEnum, auto 1from enum import IntEnum, auto
2 2
3from .generated import data_pb2 as data_pb2 3from .generated import data_pb2 as data_pb2
4from .items import SYMBOL_ITEMS
4from typing import TYPE_CHECKING, NamedTuple 5from typing import TYPE_CHECKING, NamedTuple
5 6
6from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior 7from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior
@@ -23,7 +24,6 @@ class AccessRequirements:
23 items: set[str] 24 items: set[str]
24 progressives: dict[str, int] 25 progressives: dict[str, int]
25 rooms: set[str] 26 rooms: set[str]
26 symbols: set[str]
27 letters: dict[str, int] 27 letters: dict[str, int]
28 cyans: bool 28 cyans: bool
29 29
@@ -34,7 +34,6 @@ class AccessRequirements:
34 self.items = set() 34 self.items = set()
35 self.progressives = dict() 35 self.progressives = dict()
36 self.rooms = set() 36 self.rooms = set()
37 self.symbols = set()
38 self.letters = dict() 37 self.letters = dict()
39 self.cyans = False 38 self.cyans = False
40 self.or_logic = list() 39 self.or_logic = list()
@@ -49,9 +48,6 @@ class AccessRequirements:
49 for room in other.rooms: 48 for room in other.rooms:
50 self.rooms.add(room) 49 self.rooms.add(room)
51 50
52 for symbol in other.symbols:
53 self.symbols.add(symbol)
54
55 for letter, level in other.letters.items(): 51 for letter, level in other.letters.items():
56 self.letters[letter] = max(self.letters.get(letter, 0), level) 52 self.letters[letter] = max(self.letters.get(letter, 0), level)
57 53
@@ -60,6 +56,10 @@ class AccessRequirements:
60 for disjunction in other.or_logic: 56 for disjunction in other.or_logic:
61 self.or_logic.append(disjunction) 57 self.or_logic.append(disjunction)
62 58
59 def is_empty(self) -> bool:
60 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
61 and not self.cyans and len(self.or_logic) == 0)
62
63 def __repr__(self): 63 def __repr__(self):
64 parts = [] 64 parts = []
65 if len(self.items) > 0: 65 if len(self.items) > 0:
@@ -68,8 +68,6 @@ class AccessRequirements:
68 parts.append(f"progressives={self.progressives}") 68 parts.append(f"progressives={self.progressives}")
69 if len(self.rooms) > 0: 69 if len(self.rooms) > 0:
70 parts.append(f"rooms={self.rooms}") 70 parts.append(f"rooms={self.rooms}")
71 if len(self.symbols) > 0:
72 parts.append(f"symbols={self.symbols}")
73 if len(self.letters) > 0: 71 if len(self.letters) > 0:
74 parts.append(f"letters={self.letters}") 72 parts.append(f"letters={self.letters}")
75 if self.cyans: 73 if self.cyans:
@@ -231,6 +229,10 @@ class Lingo2PlayerLogic:
231 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id, 229 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id,
232 reqs)) 230 reqs))
233 231
232 if self.world.options.shuffle_symbols:
233 for symbol_name in SYMBOL_ITEMS.values():
234 self.real_items.append(symbol_name)
235
234 def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: 236 def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements:
235 if answer is None: 237 if answer is None:
236 if panel_id not in self.panel_reqs: 238 if panel_id not in self.panel_reqs:
@@ -253,25 +255,35 @@ class Lingo2PlayerLogic:
253 self.add_solution_reqs(reqs, answer) 255 self.add_solution_reqs(reqs, answer)
254 elif len(panel.proxies) > 0: 256 elif len(panel.proxies) > 0:
255 possibilities = [] 257 possibilities = []
258 already_filled = False
256 259
257 for proxy in panel.proxies: 260 for proxy in panel.proxies:
258 proxy_reqs = AccessRequirements() 261 proxy_reqs = AccessRequirements()
259 self.add_solution_reqs(proxy_reqs, proxy.answer) 262 self.add_solution_reqs(proxy_reqs, proxy.answer)
260 263
261 possibilities.append(proxy_reqs) 264 if not proxy_reqs.is_empty():
265 possibilities.append(proxy_reqs)
266 else:
267 already_filled = True
268 break
262 269
263 if not any(proxy.answer == panel.answer for proxy in panel.proxies): 270 if not already_filled and not any(proxy.answer == panel.answer for proxy in panel.proxies):
264 proxy_reqs = AccessRequirements() 271 proxy_reqs = AccessRequirements()
265 self.add_solution_reqs(proxy_reqs, panel.answer) 272 self.add_solution_reqs(proxy_reqs, panel.answer)
266 273
267 possibilities.append(proxy_reqs) 274 if not proxy_reqs.is_empty():
275 possibilities.append(proxy_reqs)
276 else:
277 already_filled = True
268 278
269 reqs.or_logic.append(possibilities) 279 if not already_filled:
280 reqs.or_logic.append(possibilities)
270 else: 281 else:
271 self.add_solution_reqs(reqs, panel.answer) 282 self.add_solution_reqs(reqs, panel.answer)
272 283
273 for symbol in panel.symbols: 284 if self.world.options.shuffle_symbols:
274 reqs.symbols.add(symbol) 285 for symbol in panel.symbols:
286 reqs.items.add(SYMBOL_ITEMS.get(symbol))
275 287
276 if panel.HasField("required_door"): 288 if panel.HasField("required_door"):
277 door_reqs = self.get_door_open_reqs(panel.required_door) 289 door_reqs = self.get_door_open_reqs(panel.required_door)
@@ -300,9 +312,16 @@ class Lingo2PlayerLogic:
300 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) 312 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
301 reqs.merge(panel_reqs) 313 reqs.merge(panel_reqs)
302 elif door.complete_at == 1: 314 elif door.complete_at == 1:
303 reqs.or_logic.append([self.get_panel_reqs(proxy.panel, 315 disjunction = []
304 proxy.answer if proxy.HasField("answer") else None) 316 for proxy in door.panels:
305 for proxy in door.panels]) 317 proxy_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
318 if proxy_reqs.is_empty():
319 disjunction.clear()
320 break
321 else:
322 disjunction.append(proxy_reqs)
323 if len(disjunction) > 0:
324 reqs.or_logic.append(disjunction)
306 else: 325 else:
307 # TODO: Handle complete_at > 1 326 # TODO: Handle complete_at > 1
308 pass 327 pass
@@ -316,7 +335,8 @@ class Lingo2PlayerLogic:
316 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2: 335 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2:
317 reqs.rooms.add("The Repetitive - Main Room") 336 reqs.rooms.add("The Repetitive - Main Room")
318 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter: 337 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter:
319 reqs.cyans = True 338 if self.world.options.shuffle_letters != ShuffleLetters.option_unlocked:
339 reqs.cyans = True
320 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item: 340 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
321 # There shouldn't be any locations that are cyan doors. 341 # There shouldn't be any locations that are cyan doors.
322 pass 342 pass
@@ -335,7 +355,11 @@ class Lingo2PlayerLogic:
335 355
336 for ending_id in door.endings: 356 for ending_id in door.endings:
337 ending = self.world.static_logic.objects.endings[ending_id] 357 ending = self.world.static_logic.objects.endings[ending_id]
338 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)") 358
359 if self.world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
360 reqs.items.add("Victory")
361 else:
362 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)")
339 363
340 for sub_door_id in door.doors: 364 for sub_door_id in door.doors:
341 sub_reqs = self.get_door_open_reqs(sub_door_id) 365 sub_reqs = self.get_door_open_reqs(sub_door_id)
diff --git a/apworld/requirements.txt b/apworld/requirements.txt index 49ca0a7..dbc395b 100644 --- a/apworld/requirements.txt +++ b/apworld/requirements.txt
@@ -1 +1 @@
protobuf==3.20.3 \ No newline at end of file protobuf==3.20.3
diff --git a/apworld/rules.py b/apworld/rules.py index 56486fa..0bff056 100644 --- a/apworld/rules.py +++ b/apworld/rules.py
@@ -18,8 +18,6 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
18 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): 18 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms):
19 return False 19 return False
20 20
21 # TODO: symbols
22
23 for letter_key, letter_level in reqs.letters.items(): 21 for letter_key, letter_level in reqs.letters.items():
24 if not state.has(letter_key, world.player, letter_level): 22 if not state.has(letter_key, world.player, letter_level):
25 return False 23 return False
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 0cc7e55..c112d8e 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -1,4 +1,5 @@
1from .generated import data_pb2 as data_pb2 1from .generated import data_pb2 as data_pb2
2from .items import SYMBOL_ITEMS
2import pkgutil 3import pkgutil
3 4
4class Lingo2StaticLogic: 5class Lingo2StaticLogic:
@@ -64,6 +65,9 @@ class Lingo2StaticLogic:
64 65
65 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" 66 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done"
66 67
68 for symbol_name in SYMBOL_ITEMS.values():
69 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
70
67 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} 71 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()}
68 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} 72 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()}
69 73
diff --git a/client/Archipelago/gamedata.gd b/client/Archipelago/gamedata.gd index f7a5d90..d8d16ed 100644 --- a/client/Archipelago/gamedata.gd +++ b/client/Archipelago/gamedata.gd
@@ -5,15 +5,41 @@ var SCRIPT_proto
5var objects 5var objects
6var door_id_by_map_node_path = {} 6var door_id_by_map_node_path = {}
7var painting_id_by_map_node_path = {} 7var painting_id_by_map_node_path = {}
8var panel_id_by_map_node_path = {}
8var door_id_by_ap_id = {} 9var door_id_by_ap_id = {}
9var map_id_by_name = {} 10var map_id_by_name = {}
10var progressive_id_by_ap_id = {} 11var progressive_id_by_ap_id = {}
11var letter_id_by_ap_id = {} 12var letter_id_by_ap_id = {}
13var symbol_item_ids = []
14
15var kSYMBOL_ITEMS
12 16
13 17
14func _init(proto_script): 18func _init(proto_script):
15 SCRIPT_proto = proto_script 19 SCRIPT_proto = proto_script
16 20
21 kSYMBOL_ITEMS = {
22 SCRIPT_proto.PuzzleSymbol.SUN: "Sun Symbol",
23 SCRIPT_proto.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
24 SCRIPT_proto.PuzzleSymbol.ZERO: "Zero Symbol",
25 SCRIPT_proto.PuzzleSymbol.EXAMPLE: "Example Symbol",
26 SCRIPT_proto.PuzzleSymbol.BOXES: "Boxes Symbol",
27 SCRIPT_proto.PuzzleSymbol.PLANET: "Planet Symbol",
28 SCRIPT_proto.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
29 SCRIPT_proto.PuzzleSymbol.CROSS: "Cross Symbol",
30 SCRIPT_proto.PuzzleSymbol.SWEET: "Sweet Symbol",
31 SCRIPT_proto.PuzzleSymbol.GENDER: "Gender Symbol",
32 SCRIPT_proto.PuzzleSymbol.AGE: "Age Symbol",
33 SCRIPT_proto.PuzzleSymbol.SOUND: "Sound Symbol",
34 SCRIPT_proto.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
35 SCRIPT_proto.PuzzleSymbol.JOB: "Job Symbol",
36 SCRIPT_proto.PuzzleSymbol.STARS: "Stars Symbol",
37 SCRIPT_proto.PuzzleSymbol.NULL: "Null Symbol",
38 SCRIPT_proto.PuzzleSymbol.EVAL: "Eval Symbol",
39 SCRIPT_proto.PuzzleSymbol.LINGO: "Lingo Symbol",
40 SCRIPT_proto.PuzzleSymbol.QUESTION: "Question Symbol",
41 }
42
17 43
18func load(data_bytes): 44func load(data_bytes):
19 objects = SCRIPT_proto.AllObjects.new() 45 objects = SCRIPT_proto.AllObjects.new()
@@ -58,6 +84,19 @@ func load(data_bytes):
58 for letter in objects.get_letters(): 84 for letter in objects.get_letters():
59 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id() 85 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
60 86
87 for panel in objects.get_panels():
88 var room = objects.get_rooms()[panel.get_room_id()]
89 var map = objects.get_maps()[room.get_map_id()]
90
91 if not map.get_name() in panel_id_by_map_node_path:
92 panel_id_by_map_node_path[map.get_name()] = {}
93
94 var map_data = panel_id_by_map_node_path[map.get_name()]
95 map_data[panel.get_path()] = panel.get_id()
96
97 for symbol_name in kSYMBOL_ITEMS.values():
98 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
99
61 100
62func get_door_for_map_node_path(map_name, node_path): 101func get_door_for_map_node_path(map_name, node_path):
63 if not door_id_by_map_node_path.has(map_name): 102 if not door_id_by_map_node_path.has(map_name):
@@ -67,6 +106,14 @@ func get_door_for_map_node_path(map_name, node_path):
67 return map_data.get(node_path, null) 106 return map_data.get(node_path, null)
68 107
69 108
109func get_panel_for_map_node_path(map_name, node_path):
110 if not panel_id_by_map_node_path.has(map_name):
111 return null
112
113 var map_data = panel_id_by_map_node_path[map_name]
114 return map_data.get(node_path, null)
115
116
70func get_door_ap_id(door_id): 117func get_door_ap_id(door_id):
71 var door = objects.get_doors()[door_id] 118 var door = objects.get_doors()[door_id]
72 if door.has_ap_id(): 119 if door.has_ap_id():
diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd index cd0654f..383be1f 100644 --- a/client/Archipelago/manager.gd +++ b/client/Archipelago/manager.gd
@@ -1,6 +1,7 @@
1extends Node 1extends Node
2 2
3const my_version = "0.1.0" 3const MAJOR_VERSION = 1
4const MINOR_VERSION = 0
4 5
5var SCRIPT_client 6var SCRIPT_client
6var SCRIPT_keyboard 7var SCRIPT_keyboard
@@ -47,6 +48,7 @@ var keyholder_sanity = false
47var shuffle_control_center_colors = false 48var shuffle_control_center_colors = false
48var shuffle_doors = false 49var shuffle_doors = false
49var shuffle_letters = kSHUFFLE_LETTERS_VANILLA 50var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
51var shuffle_symbols = false
50var victory_condition = -1 52var victory_condition = -1
51 53
52signal could_not_connect 54signal could_not_connect
@@ -183,6 +185,11 @@ func _process_item(item, index, from, flags, amount):
183 if not letter.has_level2() or not letter.get_level2(): 185 if not letter.has_level2() or not letter.get_level2():
184 _process_key_item(letter.get_key(), amount) 186 _process_key_item(letter.get_key(), amount)
185 187
188 if gamedata.symbol_item_ids.has(item):
189 var player = get_tree().get_root().get_node_or_null("scene/player")
190 if player != null:
191 player.emit_signal("evaluate_solvability")
192
186 # Show a message about the item if it's new. 193 # Show a message about the item if it's new.
187 if index != null and index > _last_new_item: 194 if index != null and index > _last_new_item:
188 _last_new_item = index 195 _last_new_item = index
@@ -356,6 +363,7 @@ func _client_connected(slot_data):
356 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) 363 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
357 shuffle_doors = bool(slot_data.get("shuffle_doors", false)) 364 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
358 shuffle_letters = int(slot_data.get("shuffle_letters", 0)) 365 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
366 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
359 victory_condition = int(slot_data.get("victory_condition", 0)) 367 victory_condition = int(slot_data.get("victory_condition", 0))
360 368
361 # Set up item locks. 369 # Set up item locks.
diff --git a/client/Archipelago/panel.gd b/client/Archipelago/panel.gd new file mode 100644 index 0000000..fdaaf0e --- /dev/null +++ b/client/Archipelago/panel.gd
@@ -0,0 +1,101 @@
1extends "res://scripts/nodes/panel.gd"
2
3var panel_logic = null
4var symbol_solvable = true
5
6var black = load("res://assets/materials/black.material")
7
8
9func _ready():
10 super._ready()
11
12 var node_path = String(
13 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
14 )
15
16 var gamedata = global.get_node("Gamedata")
17 var panel_id = gamedata.get_panel_for_map_node_path(global.map, node_path)
18 if panel_id != null:
19 var ap = global.get_node("Archipelago")
20 if ap.shuffle_symbols:
21 if global.map == "the_entry" and node_path == "Panels/Entry/front_1":
22 clue = "i"
23 symbol = ""
24
25 setField("clue", clue)
26 setField("symbol", symbol)
27
28 panel_logic = gamedata.objects.get_panels()[panel_id]
29 checkSymbolSolvable()
30
31 if not symbol_solvable:
32 get_tree().get_root().get_node("scene/player").connect(
33 "evaluate_solvability", evaluateSolvability
34 )
35
36
37func checkSymbolSolvable():
38 var old_solvable = symbol_solvable
39 symbol_solvable = true
40
41 if panel_logic == null:
42 # There's no logic for this panel.
43 return
44
45 var ap = global.get_node("Archipelago")
46 if not ap.shuffle_symbols:
47 # Symbols aren't item-locked.
48 return
49
50 var gamedata = global.get_node("Gamedata")
51 for symbol in panel_logic.get_symbols():
52 var item_name = gamedata.kSYMBOL_ITEMS.get(symbol)
53 var item_id = gamedata.objects.get_special_ids()[item_name]
54 if ap.client.getItemAmount(item_id) < 1:
55 symbol_solvable = false
56 break
57
58 if symbol_solvable != old_solvable:
59 if symbol_solvable:
60 setField("clue", clue)
61 setField("symbol", symbol)
62 setField("answer", answer)
63 else:
64 quad_mesh.surface_set_material(0, black)
65 get_node("Hinge/clue").text = "missing"
66 get_node("Hinge/answer").text = "symbols"
67
68
69func checkSolvable(key):
70 checkSymbolSolvable()
71 if not symbol_solvable:
72 return false
73
74 return super.checkSolvable(key)
75
76
77func evaluateSolvability():
78 checkSolvable("")
79
80
81func passedInput(key, skip_focus_check = false):
82 if not symbol_solvable:
83 return
84
85 super.passedInput(key, skip_focus_check)
86
87
88func focus():
89 if not symbol_solvable:
90 has_focus = false
91 return
92
93 super.focus()
94
95
96func unfocus():
97 if not symbol_solvable:
98 has_focus = false
99 return
100
101 super.unfocus()
diff --git a/client/Archipelago/player.gd b/client/Archipelago/player.gd index dd6aa2b..4b995bc 100644 --- a/client/Archipelago/player.gd +++ b/client/Archipelago/player.gd
@@ -16,6 +16,8 @@ const kEndingNameByVictoryValue = {
16 12: "WHITE", 16 12: "WHITE",
17} 17}
18 18
19signal evaluate_solvability
20
19 21
20func _ready(): 22func _ready():
21 var khl_script = load("res://scripts/nodes/keyHolderListener.gd") 23 var khl_script = load("res://scripts/nodes/keyHolderListener.gd")
@@ -188,6 +190,11 @@ func _ready():
188 warp_enter.rotation_degrees.y = 90 190 warp_enter.rotation_degrees.y = 90
189 get_parent().add_child.call_deferred(warp_enter) 191 get_parent().add_child.call_deferred(warp_enter)
190 192
193 # Remove door behind X1.
194 if global.map == "the_entry":
195 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
196 door_node.handleTriggered()
197
191 super._ready() 198 super._ready()
192 199
193 await get_tree().process_frame 200 await get_tree().process_frame
diff --git a/client/Archipelago/settings_screen.gd b/client/Archipelago/settings_screen.gd index aaaf72a..a3bc25e 100644 --- a/client/Archipelago/settings_screen.gd +++ b/client/Archipelago/settings_screen.gd
@@ -40,6 +40,7 @@ func _ready():
40 ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd") 40 ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd")
41 ) 41 )
42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd")) 42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/panel.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd")) 44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd"))
44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd")) 45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd")) 46 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd"))
@@ -89,7 +90,7 @@ func _ready():
89 history_box.get_popup().connect("id_pressed", historySelected) 90 history_box.get_popup().connect("id_pressed", historySelected)
90 91
91 # Show client version. 92 # Show client version.
92 $Panel/title.text = "ARCHIPELAGO (%s)" % ap.my_version 93 $Panel/title.text = "ARCHIPELAGO (%d.%d)" % [ap.MAJOR_VERSION, ap.MINOR_VERSION]
93 94
94 # Increase font size in text boxes. 95 # Increase font size in text boxes.
95 $Panel/server_box.add_theme_font_size_override("font_size", 36) 96 $Panel/server_box.add_theme_font_size_override("font_size", 36)
@@ -159,6 +160,7 @@ func connectionSuccessful():
159 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn") 160 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
160 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") 161 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
161 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") 162 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
163 clearResourceCache("res://objects/nodes/panel.tscn")
162 clearResourceCache("res://objects/nodes/player.tscn") 164 clearResourceCache("res://objects/nodes/player.tscn")
163 clearResourceCache("res://objects/nodes/saver.tscn") 165 clearResourceCache("res://objects/nodes/saver.tscn")
164 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn") 166 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn")
diff --git a/data/connections.txtpb b/data/connections.txtpb index a79778f..d718c96 100644 --- a/data/connections.txtpb +++ b/data/connections.txtpb
@@ -841,6 +841,8 @@ connections {
841 } 841 }
842 oneway: true 842 oneway: true
843} 843}
844# Two one-way connections because the CLUE panel only needs to be solved to
845# go from The Great to The Partial.
844connections { 846connections {
845 from { 847 from {
846 port { 848 port {
@@ -856,6 +858,24 @@ connections {
856 name: "GREAT" 858 name: "GREAT"
857 } 859 }
858 } 860 }
861 oneway: true
862}
863connections {
864 from {
865 port {
866 map: "the_partial"
867 room: "Obverse Side"
868 name: "GREAT"
869 }
870 }
871 to {
872 port {
873 map: "the_great"
874 room: "West Side"
875 name: "PARTIAL"
876 }
877 }
878 oneway: true
859} 879}
860connections { 880connections {
861 from { 881 from {
diff --git a/data/ids.yaml b/data/ids.yaml index e2ec985..30a400b 100644 --- a/data/ids.yaml +++ b/data/ids.yaml
@@ -3836,6 +3836,25 @@ endings:
3836 YELLOW: 1206 3836 YELLOW: 1206
3837special: 3837special:
3838 A Job Well Done: 1160 3838 A Job Well Done: 1160
3839 Age Symbol: 2791
3840 Anagram Symbol: 2792
3841 Boxes Symbol: 2793
3842 Cross Symbol: 2794
3843 Eval Symbol: 2795
3844 Example Symbol: 2796
3845 Gender Symbol: 2797
3846 Job Symbol: 2798
3847 Lingo Symbol: 2799
3848 Null Symbol: 2800
3849 Planet Symbol: 2801
3850 Pyramid Symbol: 2802
3851 Question Symbol: 2803
3852 Sound Symbol: 2804
3853 Sparkles Symbol: 2805
3854 Stars Symbol: 2806
3855 Sun Symbol: 2807
3856 Sweet Symbol: 2808
3857 Zero Symbol: 2809
3839progressives: 3858progressives:
3840 Progressive Gold Ending: 2753 3859 Progressive Gold Ending: 2753
3841door_groups: 3860door_groups:
diff --git a/data/maps/the_entry/rooms/Starting Room.txtpb b/data/maps/the_entry/rooms/Starting Room.txtpb index bc77e6d..8e8373b 100644 --- a/data/maps/the_entry/rooms/Starting Room.txtpb +++ b/data/maps/the_entry/rooms/Starting Room.txtpb
@@ -24,7 +24,9 @@ panels {
24 path: "Panels/Entry/front_1" 24 path: "Panels/Entry/front_1"
25 clue: "eye" 25 clue: "eye"
26 answer: "i" 26 answer: "i"
27 symbols: ZERO 27 #symbols: ZERO
28 # This panel blocks getting N1 and T1. We will mod it to be I/I with no symbol
29 # when symbol shuffle is on.
28} 30}
29panels { 31panels {
30 name: "HINT" 32 name: "HINT"
diff --git a/data/maps/the_great/doors.txtpb b/data/maps/the_great/doors.txtpb index f0f2fde..5d0e90d 100644 --- a/data/maps/the_great/doors.txtpb +++ b/data/maps/the_great/doors.txtpb
@@ -508,3 +508,8 @@ doors {
508 receivers: "Panels/General/entry_7/teleportListener" 508 receivers: "Panels/General/entry_7/teleportListener"
509 double_letters: true 509 double_letters: true
510} 510}
511doors {
512 name: "Partial Entrance"
513 type: EVENT
514 panels { room: "West Side" name: "CLUE" }
515}
diff --git a/data/maps/the_great/rooms/West Side.txtpb b/data/maps/the_great/rooms/West Side.txtpb index daf1718..8279e16 100644 --- a/data/maps/the_great/rooms/West Side.txtpb +++ b/data/maps/the_great/rooms/West Side.txtpb
@@ -76,4 +76,5 @@ ports {
76 path: "Meshes/Blocks/Warps/worldport7" 76 path: "Meshes/Blocks/Warps/worldport7"
77 orientation: "east" 77 orientation: "east"
78 # ER with this is weird; make sure to place on the surface 78 # ER with this is weird; make sure to place on the surface
79 required_door { name: "Partial Entrance" }
79} 80}
diff --git a/data/maps/the_owl/doors.txtpb b/data/maps/the_owl/doors.txtpb index 5ec34c6..9254c2a 100644 --- a/data/maps/the_owl/doors.txtpb +++ b/data/maps/the_owl/doors.txtpb
@@ -235,7 +235,7 @@ doors {
235 type: EVENT 235 type: EVENT
236 #receivers: "Panels/Colors/owl_2/animationListener2" 236 #receivers: "Panels/Colors/owl_2/animationListener2"
237 panels { room: "Connected Area" name: "RANGE" } 237 panels { room: "Connected Area" name: "RANGE" }
238 panels { room: "R2C3 Bottom" name: "BLACK" } 238 panels { room: "Connected Area" name: "WHITE" }
239 panels { room: "Blue Room" name: "SKY" } 239 panels { room: "Blue Room" name: "SKY" }
240} 240}
241doors { 241doors {
diff --git a/data/metadata.txtpb b/data/metadata.txtpb new file mode 100644 index 0000000..ef66622 --- /dev/null +++ b/data/metadata.txtpb
@@ -0,0 +1,22 @@
1# Filler item.
2special_names: "A Job Well Done"
3# Symbol items.
4special_names: "Age Symbol"
5special_names: "Anagram Symbol"
6special_names: "Boxes Symbol"
7special_names: "Cross Symbol"
8special_names: "Eval Symbol"
9special_names: "Example Symbol"
10special_names: "Gender Symbol"
11special_names: "Job Symbol"
12special_names: "Lingo Symbol"
13special_names: "Null Symbol"
14special_names: "Planet Symbol"
15special_names: "Pyramid Symbol"
16special_names: "Question Symbol"
17special_names: "Sound Symbol"
18special_names: "Sparkles Symbol"
19special_names: "Stars Symbol"
20special_names: "Sun Symbol"
21special_names: "Sweet Symbol"
22special_names: "Zero Symbol"
diff --git a/proto/human.proto b/proto/human.proto index d48f687..1c5b463 100644 --- a/proto/human.proto +++ b/proto/human.proto
@@ -212,6 +212,10 @@ message HumanDoorGroups {
212 repeated HumanDoorGroup door_groups = 1; 212 repeated HumanDoorGroup door_groups = 1;
213} 213}
214 214
215message HumanGlobalMetadata {
216 repeated string special_names = 1;
217}
218
215message IdMappings { 219message IdMappings {
216 message RoomIds { 220 message RoomIds {
217 map<string, uint64> panels = 1; 221 map<string, uint64> panels = 1;
diff --git a/tools/assign_ids/main.cpp b/tools/assign_ids/main.cpp index ee55338..3e16f78 100644 --- a/tools/assign_ids/main.cpp +++ b/tools/assign_ids/main.cpp
@@ -44,6 +44,7 @@ class AssignIds {
44 ProcessSpecialIds(); 44 ProcessSpecialIds();
45 ProcessProgressivesFile(datadir_path / "progressives.txtpb"); 45 ProcessProgressivesFile(datadir_path / "progressives.txtpb");
46 ProcessDoorGroupsFile(datadir_path / "door_groups.txtpb"); 46 ProcessDoorGroupsFile(datadir_path / "door_groups.txtpb");
47 ProcessGlobalMetadataFile(datadir_path / "metadata.txtpb");
47 48
48 WriteIds(ids_path); 49 WriteIds(ids_path);
49 50
@@ -288,6 +289,23 @@ class AssignIds {
288 } 289 }
289 } 290 }
290 291
292 void ProcessGlobalMetadataFile(std::filesystem::path path) {
293 if (!std::filesystem::exists(path)) {
294 return;
295 }
296
297 auto h_metadata = ReadMessageFromFile<HumanGlobalMetadata>(path.string());
298 auto& specials = *output_.mutable_special();
299
300 for (const std::string& h_special : h_metadata.special_names()) {
301 if (!id_mappings_.special().contains(h_special)) {
302 specials[h_special] = next_id_++;
303 } else {
304 specials[h_special] = id_mappings_.special().at(h_special);
305 }
306 }
307 }
308
291 private: 309 private:
292 void UpdateNextId(const google::protobuf::Map<std::string, uint64_t>& ids) { 310 void UpdateNextId(const google::protobuf::Map<std::string, uint64_t>& ids) {
293 for (const auto& [_, id] : ids) { 311 for (const auto& [_, id] : ids) {