about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md59
-rw-r--r--apworld/__init__.py3
-rw-r--r--apworld/client/manager.gd13
-rw-r--r--apworld/client/maps/control_center.gd35
-rw-r--r--apworld/client/player.gd11
-rw-r--r--apworld/options.py20
-rw-r--r--apworld/player_logic.py8
-rw-r--r--apworld/regions.py4
-rw-r--r--apworld/static_logic.py2
-rw-r--r--data/maps/control_center/connections.txtpb1
-rw-r--r--data/metadata.txtpb2
-rw-r--r--proto/data.proto1
-rw-r--r--proto/human.proto4
-rw-r--r--tools/datapacker/main.cpp5
14 files changed, 165 insertions, 3 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3740ed5..d6ca532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,64 @@
1# lingo2-archipelago Releases 1# lingo2-archipelago Releases
2 2
3## v9.1.0 - 2026-02-08
4
5- Added "Custom Mint Ending" option. This creates a panel in the Control Center
6 after putting EXIT into the keyholders, the answer to which is the value of
7 the option. The player must solve the panel to spawn Mint Ending.
8- Added "Shuffle Music" option. This randomizes the music track that plays on
9 each map.
10- Added an item group for symbols.
11- Fix the message on the Control Center wall overcounting the number of endings
12 by 1.
13
14Compatibility notes:
15
16- This client should be completely compatible with worlds generated on v9.0.0.
17
18Download:
19[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v9.1.0/lingo2.apworld)<br/>
20Template YAML:
21[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v9.1.0/Lingo%202.yaml)<br/>
22Source: [v9.1.0](https://code.fourisland.com/lingo2-archipelago/tag/?h=v9.1.0)
23
24## v9.0.0 - 2026-02-07
25
26- Added "Shuffle Fast Travel" option. This allows you to randomize the
27 destinations of the fast travel buttons on the pause menu to almost any map
28 (except for The Entry, which is always the center button).
29- Added "Fast Travel Access" option. By default, fast travel is unlocked by
30 entering the destination area manually. You can instead set to have all fast
31 travel destinations unlocked from the start, or have them locked behind items
32 (apart from The Entry).
33- Added "Restrict Letter Placements" option. When enabled, letter items will
34 only be placed in your local world, in letter locations. This only has an
35 effect when Shuffle Letters is set to Item Cyan or Progressive. This is
36 experimental, and may slow down generation.
37- The values of the Endings Requirement and Masteries Requirement options are
38 now shown in the Control Center, as well as how many endings and masteries you
39 currently have.
40- Previously, vanilla doors logic expected you to have to solve every panel in
41 the Daedalus Computer Room in order to use the back exit door. This has been
42 fixed to only require solving one panel.
43- Previously, shuffled doors + shuffled worldports logic expected you to be able
44 to enter Control Center from the Perceptive Entrance without the door item by
45 solving the PART panel (as you would in vanilla doors). This has been fixed so
46 that the door item is required in both directions.
47
48Compatibility notes:
49
50- The change to the door in the Control Center required creating a new item with
51 a new ID, which is not present in worlds generated on older versions of the
52 apworld. Therefore, if you have an older world using door shuffle, you may not
53 be able to access the checks in the Perceptive Entrance or the worldport (if
54 worldports are shuffled).
55
56Download:
57[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v9.0.0/lingo2.apworld)<br/>
58Template YAML:
59[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v9.0.0/Lingo%202.yaml)<br/>
60Source: [v9.0.0](https://code.fourisland.com/lingo2-archipelago/tag/?h=v9.0.0)
61
3## v8.1.1 - 2026-02-05 62## v8.1.1 - 2026-02-05
4 63
5- Fixed issue in Daedalus Only mode where the Lavender Cubes and Rainbow Rooms 64- Fixed issue in Daedalus Only mode where the Lavender Cubes and Rainbow Rooms
diff --git a/apworld/__init__.py b/apworld/__init__.py index ba5d7ea..6b5338e 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -157,6 +157,7 @@ class Lingo2World(World):
157 "shuffle_doors", 157 "shuffle_doors",
158 "shuffle_gallery_paintings", 158 "shuffle_gallery_paintings",
159 "shuffle_letters", 159 "shuffle_letters",
160 "shuffle_music",
160 "shuffle_symbols", 161 "shuffle_symbols",
161 "shuffle_worldports", 162 "shuffle_worldports",
162 "strict_cyan_ending", 163 "strict_cyan_ending",
@@ -166,7 +167,9 @@ class Lingo2World(World):
166 167
167 slot_data: dict[str, object] = { 168 slot_data: dict[str, object] = {
168 **self.options.as_dict(*slot_options), 169 **self.options.as_dict(*slot_options),
170 "custom_mint_ending": self.player_logic.custom_mint_ending or "",
169 "rte": [self.static_logic.objects.maps[map_id].name for map_id in self.player_logic.rte_mapping], 171 "rte": [self.static_logic.objects.maps[map_id].name for map_id in self.player_logic.rte_mapping],
172 "seed": self.random.randint(0, 1000000),
170 "version": self.static_logic.get_data_version(), 173 "version": self.static_logic.get_data_version(),
171 } 174 }
172 175
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 00f03ea..f10a0b7 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd
@@ -67,6 +67,7 @@ const kEndingNameByVictoryValue = {
67} 67}
68 68
69var apworld_version = [0, 0, 0] 69var apworld_version = [0, 0, 0]
70var custom_mint_ending = ""
70var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 71var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
71var daedalus_only = false 72var daedalus_only = false
72var daedalus_roof_access = false 73var daedalus_roof_access = false
@@ -76,6 +77,7 @@ var endings_requirement = 0
76var fast_travel_access = 0 77var fast_travel_access = 0
77var keyholder_sanity = false 78var keyholder_sanity = false
78var masteries_requirement = 0 79var masteries_requirement = 0
80var music_mapping = {}
79var port_pairings = {} 81var port_pairings = {}
80var rte_mapping = [] 82var rte_mapping = []
81var shuffle_control_center_colors = false 83var shuffle_control_center_colors = false
@@ -84,6 +86,7 @@ var shuffle_gallery_paintings = false
84var shuffle_letters = kSHUFFLE_LETTERS_VANILLA 86var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
85var shuffle_symbols = false 87var shuffle_symbols = false
86var shuffle_worldports = false 88var shuffle_worldports = false
89var slot_rng = null
87var strict_cyan_ending = false 90var strict_cyan_ending = false
88var strict_purple_ending = false 91var strict_purple_ending = false
89var victory_condition = -1 92var victory_condition = -1
@@ -476,6 +479,7 @@ func _client_connected(slot_data):
476 _last_new_item = localdata[0] 479 _last_new_item = localdata[0]
477 480
478 # Read slot data. 481 # Read slot data.
482 custom_mint_ending = slot_data.get("custom_mint_ending", "")
479 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) 483 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
480 daedalus_only = bool(slot_data.get("daedalus_only", false)) 484 daedalus_only = bool(slot_data.get("daedalus_only", false))
481 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) 485 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
@@ -514,6 +518,15 @@ func _client_connected(slot_data):
514 if slot_data.has("rte"): 518 if slot_data.has("rte"):
515 rte_mapping = slot_data.get("rte") 519 rte_mapping = slot_data.get("rte")
516 520
521 slot_rng = RandomNumberGenerator.new()
522 slot_rng.seed = int(slot_data.get("seed", 0))
523
524 music_mapping.clear()
525 if bool(slot_data.get("shuffle_music", false)):
526 for map_name in global.reserved_scenes:
527 var track_index = slot_rng.randi_range(0, musicPlayer.all_tracks.size() - 1)
528 music_mapping[map_name] = musicPlayer.all_tracks.keys()[track_index]
529
517 # Set up item locks. 530 # Set up item locks.
518 _item_locks = {} 531 _item_locks = {}
519 532
diff --git a/apworld/client/maps/control_center.gd b/apworld/client/maps/control_center.gd index 92999d3..8e919ab 100644 --- a/apworld/client/maps/control_center.gd +++ b/apworld/client/maps/control_center.gd
@@ -79,7 +79,7 @@ func on_map_load(root):
79 var mastery_count = 0 79 var mastery_count = 0
80 for key in unlocks.data: 80 for key in unlocks.data:
81 if unlocks.data[key] == "unlocked": 81 if unlocks.data[key] == "unlocked":
82 if key.ends_with("_ending"): 82 if key.ends_with("_ending") and key != "free_ending":
83 ending_count += 1 83 ending_count += 1
84 elif key.ends_with("_mastery"): 84 elif key.ends_with("_mastery"):
85 mastery_count += 1 85 mastery_count += 1
@@ -96,6 +96,39 @@ func on_map_load(root):
96 sign2.text = "Masteries: %d/%d" % [mastery_count, ap.masteries_requirement] 96 sign2.text = "Masteries: %d/%d" % [mastery_count, ap.masteries_requirement]
97 root.get_node("/root/scene").add_child.call_deferred(sign2) 97 root.get_node("/root/scene").add_child.call_deferred(sign2)
98 98
99 # Handle custom Mint Ending.
100 if ap.custom_mint_ending != "":
101 var panel_prefab = preload("res://objects/nodes/panel.tscn")
102 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
103
104 var mint_ending = root.get_node("/root/scene/Components/Endings/mint_ending")
105
106 var mint_panel = panel_prefab.instantiate()
107 mint_panel.name = "mint_panel"
108 mint_panel.clue = ap.custom_mint_ending
109 mint_panel.symbol = ""
110 mint_panel.answer = ap.custom_mint_ending
111 mint_panel.position = Vector3(-63, 3, -29)
112 mint_panel.rotation_degrees = Vector3(-45, 90, 0)
113 root.get_node("/root/scene").add_child.call_deferred(mint_panel)
114
115 var mint_tpl = tpl_prefab.instantiate()
116 mint_tpl.name = "mint_tpl"
117 mint_tpl.teleport_point = mint_ending.position
118 mint_tpl.teleport_rotate = mint_ending.rotation_degrees
119 mint_tpl.target_path = mint_ending
120 mint_tpl.senders.append(NodePath("/root/scene/mint_panel"))
121 root.get_node("/root/scene").add_child.call_deferred(mint_tpl)
122
123 var mint_tpl2 = tpl_prefab.instantiate()
124 mint_tpl2.name = "mint_tpl2"
125 mint_tpl2.teleport_point = Vector3(0, -1000, 0)
126 mint_tpl2.target_path = mint_panel
127 mint_tpl2.senders.append(NodePath("/root/scene/mint_panel"))
128 root.get_node("/root/scene").add_child.call_deferred(mint_tpl2)
129
130 mint_ending.position.y = -1000
131
99 132
100func _set_up_mastery_listener(root, name): 133func _set_up_mastery_listener(root, name):
101 var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn") 134 var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn")
diff --git a/apworld/client/player.gd b/apworld/client/player.gd index 95c05d7..dabc15d 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd
@@ -196,6 +196,17 @@ func _ready():
196 minimap.visible = ap.show_minimap 196 minimap.visible = ap.show_minimap
197 get_parent().add_child.call_deferred(minimap) 197 get_parent().add_child.call_deferred(minimap)
198 198
199 if ap.music_mapping.has(global.map):
200 var song_setter = get_node_or_null("/root/scene/songSetter")
201 if song_setter:
202 song_setter.song_name = ap.music_mapping[global.map]
203 else:
204 var song_setter_prefab = preload("res://objects/nodes/songSetter.tscn")
205 song_setter = song_setter_prefab.instantiate()
206 song_setter.name = "songSetter"
207 song_setter.song_name = ap.music_mapping[global.map]
208 get_parent().add_child.call_deferred(song_setter)
209
199 super._ready() 210 super._ready()
200 211
201 await get_tree().process_frame 212 await get_tree().process_frame
diff --git a/apworld/options.py b/apworld/options.py index 6fe6d8d..c1eab33 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, OptionSet 3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet, FreeText
4 4
5 5
6class ShuffleDoors(DefaultOnToggle): 6class ShuffleDoors(DefaultOnToggle):
@@ -178,6 +178,15 @@ class DaedalusRoofAccess(Toggle):
178 display_name = "Allow Daedalus Roof Access" 178 display_name = "Allow Daedalus Roof Access"
179 179
180 180
181class CustomMintEnding(FreeText):
182 """
183 If not blank, this will add a new panel that must be solved before collecting Mint Ending (EXIT in the Control
184 Center). The panel will only require typing the text provided for this option, which means the choice of letters
185 here has an impact on logic.
186 """
187 display_name = "Custom Mint Ending"
188
189
181class StrictPurpleEnding(DefaultOnToggle): 190class StrictPurpleEnding(DefaultOnToggle):
182 """ 191 """
183 If enabled, the player will be required to have all purple (level 1) letters in order to get Purple Ending. 192 If enabled, the player will be required to have all purple (level 1) letters in order to get Purple Ending.
@@ -256,6 +265,13 @@ class TrapPercentage(Range):
256 default = 0 265 default = 0
257 266
258 267
268class ShuffleMusic(Toggle):
269 """
270 If enabled, every map will be assigned a random music track.
271 """
272 display_name = "Shuffle Music"
273
274
259@dataclass 275@dataclass
260class Lingo2Options(PerGameCommonOptions): 276class Lingo2Options(PerGameCommonOptions):
261 shuffle_doors: ShuffleDoors 277 shuffle_doors: ShuffleDoors
@@ -273,9 +289,11 @@ class Lingo2Options(PerGameCommonOptions):
273 enable_gift_maps: EnableGiftMaps 289 enable_gift_maps: EnableGiftMaps
274 daedalus_only: DaedalusOnly 290 daedalus_only: DaedalusOnly
275 daedalus_roof_access: DaedalusRoofAccess 291 daedalus_roof_access: DaedalusRoofAccess
292 custom_mint_ending: CustomMintEnding
276 strict_purple_ending: StrictPurpleEnding 293 strict_purple_ending: StrictPurpleEnding
277 strict_cyan_ending: StrictCyanEnding 294 strict_cyan_ending: StrictCyanEnding
278 victory_condition: VictoryCondition 295 victory_condition: VictoryCondition
279 endings_requirement: EndingsRequirement 296 endings_requirement: EndingsRequirement
280 masteries_requirement: MasteriesRequirement 297 masteries_requirement: MasteriesRequirement
281 trap_percentage: TrapPercentage 298 trap_percentage: TrapPercentage
299 shuffle_music: ShuffleMusic
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 7bfd49f..ea74266 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -223,6 +223,7 @@ class Lingo2PlayerLogic:
223 double_letter_amount: dict[str, int] 223 double_letter_amount: dict[str, int]
224 goal_room_id: int 224 goal_room_id: int
225 rte_mapping: list[int] 225 rte_mapping: list[int]
226 custom_mint_ending: str | None
226 227
227 def __init__(self, world: "Lingo2World"): 228 def __init__(self, world: "Lingo2World"):
228 self.world = world 229 self.world = world
@@ -237,6 +238,7 @@ class Lingo2PlayerLogic:
237 self.real_items = list() 238 self.real_items = list()
238 self.starting_items = list() 239 self.starting_items = list()
239 self.double_letter_amount = dict() 240 self.double_letter_amount = dict()
241 self.custom_mint_ending = None
240 242
241 def should_shuffle_map(game_map) -> bool | set[int]: 243 def should_shuffle_map(game_map) -> bool | set[int]:
242 if world.options.daedalus_only: 244 if world.options.daedalus_only:
@@ -302,6 +304,12 @@ class Lingo2PlayerLogic:
302 raise OptionError(f"When Restrict Letter Placements is enabled and Shuffle Letters is set to Progressive, " 304 raise OptionError(f"When Restrict Letter Placements is enabled and Shuffle Letters is set to Progressive, "
303 f"both Shuffle Doors and Shuffle Symbols must be disabled (Player {world.player}).") 305 f"both Shuffle Doors and Shuffle Symbols must be disabled (Player {world.player}).")
304 306
307 if world.options.custom_mint_ending.value != "":
308 self.custom_mint_ending = ''.join(filter(str.isalpha, world.options.custom_mint_ending.value)).lower()
309
310 if len(self.custom_mint_ending) > 52:
311 raise OptionError(f"Custom Mint Ending should not be greater than 52 letters (Player {world.player}).")
312
305 maximum_masteries = 13 + len(world.options.enable_gift_maps.value) 313 maximum_masteries = 13 + len(world.options.enable_gift_maps.value)
306 if world.options.enable_icarus: 314 if world.options.enable_icarus:
307 maximum_masteries += 1 315 maximum_masteries += 1
diff --git a/apworld/regions.py b/apworld/regions.py index 076c143..3996153 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -141,6 +141,10 @@ def create_regions(world: "Lingo2World"):
141 if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending: 141 if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending:
142 world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz") 142 world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
143 143
144 if (connection.HasField("mint_ending") and connection.mint_ending
145 and world.player_logic.custom_mint_ending is not None):
146 world.player_logic.add_solution_reqs(reqs, world.player_logic.custom_mint_ending)
147
144 reqs.simplify() 148 reqs.simplify()
145 reqs.remove_room(from_region) 149 reqs.remove_room(from_region)
146 150
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 672ae5a..48ad78e 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -75,8 +75,10 @@ class Lingo2StaticLogic:
75 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" 75 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done"
76 self.item_id_to_name[self.objects.special_ids["Numbers"]] = "Numbers" 76 self.item_id_to_name[self.objects.special_ids["Numbers"]] = "Numbers"
77 77
78 self.item_name_groups["Symbols"] = []
78 for symbol_name in SYMBOL_ITEMS.values(): 79 for symbol_name in SYMBOL_ITEMS.values():
79 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name 80 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
81 self.item_name_groups["Symbols"].append(symbol_name)
80 82
81 for trap_name in ANTI_COLLECTABLE_TRAPS: 83 for trap_name in ANTI_COLLECTABLE_TRAPS:
82 self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name 84 self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name
diff --git a/data/maps/control_center/connections.txtpb b/data/maps/control_center/connections.txtpb index 432d39d..5dc2890 100644 --- a/data/maps/control_center/connections.txtpb +++ b/data/maps/control_center/connections.txtpb
@@ -17,6 +17,7 @@ connections {
17 from_room: "Main Area" 17 from_room: "Main Area"
18 to_room: "Mint Ending" 18 to_room: "Mint Ending"
19 door { name: "Mint Ending Door" } 19 door { name: "Mint Ending Door" }
20 mint_ending: true
20} 21}
21connections { 22connections {
22 from_room: "Main Area" 23 from_room: "Main Area"
diff --git a/data/metadata.txtpb b/data/metadata.txtpb index c362be0..99d3021 100644 --- a/data/metadata.txtpb +++ b/data/metadata.txtpb
@@ -1,6 +1,6 @@
1version { 1version {
2 major: 9 2 major: 9
3 minor: 0 3 minor: 1
4 patch: 0 4 patch: 0
5} 5}
6# Filler item. 6# Filler item.
diff --git a/proto/data.proto b/proto/data.proto index e053942..619b3d3 100644 --- a/proto/data.proto +++ b/proto/data.proto
@@ -136,6 +136,7 @@ message Connection {
136 optional bool roof_access = 7; 136 optional bool roof_access = 7;
137 optional bool purple_ending = 8; 137 optional bool purple_ending = 8;
138 optional bool cyan_ending = 9; 138 optional bool cyan_ending = 9;
139 optional bool mint_ending = 11;
139 optional bool vanilla_only = 10; 140 optional bool vanilla_only = 10;
140} 141}
141 142
diff --git a/proto/human.proto b/proto/human.proto index 6c98d3f..5cd8ce7 100644 --- a/proto/human.proto +++ b/proto/human.proto
@@ -79,6 +79,10 @@ message HumanConnection {
79 // when the Strict Cyan Ending option is on. 79 // when the Strict Cyan Ending option is on.
80 optional bool cyan_ending = 10; 80 optional bool cyan_ending = 10;
81 81
82 // This means that the connection should additionally require being able to
83 // type a specific text string when Custom Mint Ending is on.
84 optional bool mint_ending = 12;
85
82 // This means that the connection only exists when doors are not shuffled. 86 // This means that the connection only exists when doors are not shuffled.
83 optional bool vanilla_only = 11; 87 optional bool vanilla_only = 11;
84} 88}
diff --git a/tools/datapacker/main.cpp b/tools/datapacker/main.cpp index 7313fee..4ecde74 100644 --- a/tools/datapacker/main.cpp +++ b/tools/datapacker/main.cpp
@@ -533,6 +533,11 @@ class DataPacker {
533 r_connection.set_cyan_ending(human_connection.cyan_ending()); 533 r_connection.set_cyan_ending(human_connection.cyan_ending());
534 } 534 }
535 535
536 if (human_connection.has_mint_ending()) {
537 f_connection.set_mint_ending(human_connection.mint_ending());
538 r_connection.set_mint_ending(human_connection.mint_ending());
539 }
540
536 if (human_connection.has_vanilla_only()) { 541 if (human_connection.has_vanilla_only()) {
537 f_connection.set_vanilla_only(human_connection.vanilla_only()); 542 f_connection.set_vanilla_only(human_connection.vanilla_only());
538 r_connection.set_vanilla_only(human_connection.vanilla_only()); 543 r_connection.set_vanilla_only(human_connection.vanilla_only());