about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
Diffstat (limited to 'apworld')
-rw-r--r--apworld/README.md48
-rw-r--r--apworld/__init__.py14
-rw-r--r--apworld/docs/en_Lingo_2.md4
-rw-r--r--apworld/options.py31
-rw-r--r--apworld/player_logic.py68
-rw-r--r--apworld/requirements.txt2
-rw-r--r--apworld/static_logic.py13
7 files changed, 167 insertions, 13 deletions
diff --git a/apworld/README.md b/apworld/README.md new file mode 100644 index 0000000..13374b2 --- /dev/null +++ b/apworld/README.md
@@ -0,0 +1,48 @@
1# Lingo 2 Apworld
2
3The Lingo 2 Apworld allows you to generate Archipelago Multiworlds containing
4Lingo 2.
5
6## Installation
7
81. Download the Lingo 2 Apworld from
9 [the releases page](https://code.fourisland.com/lingo2-archipelago/about/apworld/CHANGELOG.md).
102. If you do not already have it, download and install the
11 [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/).
123. Double click on `lingo2.apworld` to install it, or copy it manually to the
13 `custom_worlds` folder of your Archipelago installation.
14
15## Running from source
16
17The apworld is mostly written in Python, which does not need to be compiled.
18However, there are two files that need to be generated before the apworld can be
19used.
20
21The first file is `data.binpb`, the datafile containing the randomizer logic.
22You can read about how to generate it on
23[its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md).
24Once you have it, put it in a subfolder of `apworld` called `generated`.
25
26The second generated file is `data_pb2.py`. This file allows Archipelago to read
27the datafile. We use `protoc`, the Protocol Buffer compiler, to generate it. As
28of 0.6.3, Archipelago has protobuf 3.20.3 packaged with it, which means we need
29to compile our proto file with a similar version.
30
31If you followed the steps to generate `data.binpb` and compiled the `datapacker`
32tool yourself, you will already have protobuf version 3.21.12 installed through
33vcpkg. You can then run a command similar to this in order to generate the
34python file.
35
36```shell
37.\out\build\x64-Debug\vcpkg_installed\x64-windows\tools\protobuf\protoc.exe -Iproto\ ^
38 --python_out=apworld\generated\ .\proto\data.proto
39```
40
41The exact path to `protoc.exe` is going to depend on where vcpkg installed its
42packages. The above location is where Visual Studio will probably put it.
43
44After generating those two files, the apworld should be functional. You can copy
45it into an Archipelago source tree (rename the folder `apworld` to `lingo2` if
46you do so) if you want to edit/debug the code. Otherwise, you can zip up the
47folder and rename it to `lingo2.apworld` in order to package it for
48distribution.
diff --git a/apworld/__init__.py b/apworld/__init__.py index 8e3066d..6eeee74 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
@@ -13,6 +13,14 @@ from .static_logic import Lingo2StaticLogic
13class Lingo2WebWorld(WebWorld): 13class Lingo2WebWorld(WebWorld):
14 rich_text_options_doc = True 14 rich_text_options_doc = True
15 theme = "grass" 15 theme = "grass"
16 tutorials = [Tutorial(
17 "Multiworld Setup Guide",
18 "A guide to playing Lingo 2 with Archipelago.",
19 "English",
20 "en_Lingo_2.md",
21 "setup/en",
22 ["hatkirby"]
23 )]
16 24
17 25
18class Lingo2World(World): 26class Lingo2World(World):
@@ -32,6 +40,8 @@ class Lingo2World(World):
32 static_logic = Lingo2StaticLogic() 40 static_logic = Lingo2StaticLogic()
33 item_name_to_id = static_logic.item_name_to_id 41 item_name_to_id = static_logic.item_name_to_id
34 location_name_to_id = static_logic.location_name_to_id 42 location_name_to_id = static_logic.location_name_to_id
43 item_name_groups = static_logic.item_name_groups
44 location_name_groups = static_logic.location_name_groups
35 45
36 player_logic: Lingo2PlayerLogic 46 player_logic: Lingo2PlayerLogic
37 47
@@ -66,8 +76,10 @@ class Lingo2World(World):
66 76
67 def fill_slot_data(self): 77 def fill_slot_data(self):
68 slot_options = [ 78 slot_options = [
79 "cyan_door_behavior",
69 "daedalus_roof_access", 80 "daedalus_roof_access",
70 "keyholder_sanity", 81 "keyholder_sanity",
82 "shuffle_control_center_colors",
71 "shuffle_doors", 83 "shuffle_doors",
72 "shuffle_letters", 84 "shuffle_letters",
73 "victory_condition", 85 "victory_condition",
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/options.py b/apworld/options.py index f7dc5bd..2197b0f 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -8,6 +8,14 @@ class ShuffleDoors(Toggle):
8 display_name = "Shuffle Doors" 8 display_name = "Shuffle Doors"
9 9
10 10
11class ShuffleControlCenterColors(Toggle):
12 """
13 Some doors open after solving the COLOR panel in the Control Center. If this option is enabled, these doors will
14 instead open upon receiving an item.
15 """
16 display_name = "Shuffle Control Center Colors"
17
18
11class ShuffleLetters(Choice): 19class ShuffleLetters(Choice):
12 """ 20 """
13 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla 21 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla
@@ -40,6 +48,27 @@ class KeyholderSanity(Toggle):
40 display_name = "Keyholder Sanity" 48 display_name = "Keyholder Sanity"
41 49
42 50
51class CyanDoorBehavior(Choice):
52 """
53 Cyan-colored doors usually only open upon unlocking double letters. Some panels also only appear upon unlocking
54 double letters. This option determines how these unlocks should behave.
55
56 - **Collect H2**: In the base game, H2 is the first double letter you are intended to collect, so cyan doors only
57 open when you collect the H2 pickup in The Repetitive. Collecting the actual pickup is still required even with
58 remote letter shuffle enabled.
59 - **Any Double Letter**: Cyan doors will open when you have unlocked any cyan letter on your keyboard. In letter
60 shuffle, this means receiving a cyan letter, not picking up a cyan letter collectable.
61 - **Item**: Cyan doors will be grouped together in a single item.
62
63 Note that some cyan doors are impacted by door shuffle (e.g. the entrance to The Tower). When door shuffle is
64 enabled, these doors won't be affected by the value of this option.
65 """
66 display_name = "Cyan Door Behavior"
67 option_collect_h2 = 0
68 option_any_double_letter = 1
69 option_item = 2
70
71
43class DaedalusRoofAccess(Toggle): 72class DaedalusRoofAccess(Toggle):
44 """ 73 """
45 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 74 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus
@@ -71,7 +100,9 @@ class VictoryCondition(Choice):
71@dataclass 100@dataclass
72class Lingo2Options(PerGameCommonOptions): 101class Lingo2Options(PerGameCommonOptions):
73 shuffle_doors: ShuffleDoors 102 shuffle_doors: ShuffleDoors
103 shuffle_control_center_colors: ShuffleControlCenterColors
74 shuffle_letters: ShuffleLetters 104 shuffle_letters: ShuffleLetters
75 keyholder_sanity: KeyholderSanity 105 keyholder_sanity: KeyholderSanity
106 cyan_door_behavior: CyanDoorBehavior
76 daedalus_roof_access: DaedalusRoofAccess 107 daedalus_roof_access: DaedalusRoofAccess
77 victory_condition: VictoryCondition 108 victory_condition: VictoryCondition
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 5cb9011..dbd340c 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -3,7 +3,7 @@ from enum import IntEnum, auto
3from .generated import data_pb2 as data_pb2 3from .generated import data_pb2 as data_pb2
4from typing import TYPE_CHECKING, NamedTuple 4from typing import TYPE_CHECKING, NamedTuple
5 5
6from .options import VictoryCondition, ShuffleLetters 6from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior
7 7
8if TYPE_CHECKING: 8if TYPE_CHECKING:
9 from . import Lingo2World 9 from . import Lingo2World
@@ -123,16 +123,57 @@ class Lingo2PlayerLogic:
123 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) 123 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
124 self.real_items.append(progressive.name) 124 self.real_items.append(progressive.name)
125 125
126 for door_group in world.static_logic.objects.door_groups:
127 if door_group.type == data_pb2.DoorGroupType.CONNECTOR:
128 if not self.world.options.shuffle_doors:
129 continue
130 elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR:
131 if not self.world.options.shuffle_control_center_colors:
132 continue
133 elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP:
134 if not self.world.options.shuffle_doors:
135 continue
136 else:
137 continue
138
139 for door in door_group.doors:
140 self.item_by_door[door] = (door_group.name, 1)
141
142 self.real_items.append(door_group.name)
143
126 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled 144 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
127 # before we calculate any access requirements. 145 # before we calculate any access requirements.
128 for door in world.static_logic.objects.doors: 146 for door in world.static_logic.objects.doors:
129 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: 147 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
130 if door.id in self.item_by_door: 148 continue
149
150 if door.id in self.item_by_door:
151 continue
152
153 if (door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and
154 not self.world.options.shuffle_doors):
155 continue
156
157 if (door.type == data_pb2.DoorType.CONTROL_CENTER_COLOR and
158 not self.world.options.shuffle_control_center_colors):
159 continue
160
161 door_item_name = self.world.static_logic.get_door_item_name(door)
162 self.item_by_door[door.id] = (door_item_name, 1)
163 self.real_items.append(door_item_name)
164
165 # We handle cyan_door_behavior = Item after door shuffle, because cyan doors that are impacted by door shuffle
166 # should be exempt from cyan_door_behavior.
167 if world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
168 for door_group in world.static_logic.objects.door_groups:
169 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS:
131 continue 170 continue
132 171
133 door_item_name = self.world.static_logic.get_door_item_name(door) 172 for door in door_group.doors:
134 self.item_by_door[door.id] = (door_item_name, 1) 173 if not door in self.item_by_door:
135 self.real_items.append(door_item_name) 174 self.item_by_door[door] = (door_group.name, 1)
175
176 self.real_items.append(door_group.name)
136 177
137 for door in world.static_logic.objects.doors: 178 for door in world.static_logic.objects.doors:
138 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 179 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
@@ -272,12 +313,13 @@ class Lingo2PlayerLogic:
272 self.add_solution_reqs(reqs, door.control_center_color) 313 self.add_solution_reqs(reqs, door.control_center_color)
273 314
274 if door.double_letters: 315 if door.double_letters:
275 if self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla, 316 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2:
276 ShuffleLetters.option_vanilla_cyan]:
277 reqs.rooms.add("The Repetitive - Main Room") 317 reqs.rooms.add("The Repetitive - Main Room")
278 elif self.world.options.shuffle_letters in [ShuffleLetters.option_progressive, 318 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter:
279 ShuffleLetters.option_item_cyan]:
280 reqs.cyans = True 319 reqs.cyans = True
320 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
321 # There shouldn't be any locations that are cyan doors.
322 pass
281 323
282 for keyholder_uses in door.keyholders: 324 for keyholder_uses in door.keyholders:
283 key_name = keyholder_uses.key.upper() 325 key_name = keyholder_uses.key.upper()
@@ -293,7 +335,11 @@ class Lingo2PlayerLogic:
293 335
294 for ending_id in door.endings: 336 for ending_id in door.endings:
295 ending = self.world.static_logic.objects.endings[ending_id] 337 ending = self.world.static_logic.objects.endings[ending_id]
296 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)") 338
339 if self.world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
340 reqs.items.add("Victory")
341 else:
342 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)")
297 343
298 for sub_door_id in door.doors: 344 for sub_door_id in door.doors:
299 sub_reqs = self.get_door_open_reqs(sub_door_id) 345 sub_reqs = self.get_door_open_reqs(sub_door_id)
diff --git a/apworld/requirements.txt b/apworld/requirements.txt index b701d11..dbc395b 100644 --- a/apworld/requirements.txt +++ b/apworld/requirements.txt
@@ -1 +1 @@
protobuf>=5.29.3 \ No newline at end of file protobuf==3.20.3
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index b699d59..0cc7e55 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -8,9 +8,14 @@ class Lingo2StaticLogic:
8 item_name_to_id: dict[str, int] 8 item_name_to_id: dict[str, int]
9 location_name_to_id: dict[str, int] 9 location_name_to_id: dict[str, int]
10 10
11 item_name_groups: dict[str, list[str]]
12 location_name_groups: dict[str, list[str]]
13
11 def __init__(self): 14 def __init__(self):
12 self.item_id_to_name = {} 15 self.item_id_to_name = {}
13 self.location_id_to_name = {} 16 self.location_id_to_name = {}
17 self.item_name_groups = {}
18 self.location_name_groups = {}
14 19
15 file = pkgutil.get_data(__name__, "generated/data.binpb") 20 file = pkgutil.get_data(__name__, "generated/data.binpb")
16 self.objects = data_pb2.AllObjects() 21 self.objects = data_pb2.AllObjects()
@@ -29,25 +34,33 @@ class Lingo2StaticLogic:
29 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" 34 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
30 location_name = f"{self.get_room_object_map_name(letter)} - {letter_name}" 35 location_name = f"{self.get_room_object_map_name(letter)} - {letter_name}"
31 self.location_id_to_name[letter.ap_id] = location_name 36 self.location_id_to_name[letter.ap_id] = location_name
37 self.location_name_groups.setdefault("Letters", []).append(location_name)
32 38
33 if not letter.level2: 39 if not letter.level2:
34 self.item_id_to_name[letter.ap_id] = letter.key.upper() 40 self.item_id_to_name[letter.ap_id] = letter.key.upper()
41 self.item_name_groups.setdefault("Letters", []).append(letter.key.upper())
35 42
36 for mastery in self.objects.masteries: 43 for mastery in self.objects.masteries:
37 location_name = f"{self.get_room_object_map_name(mastery)} - Mastery" 44 location_name = f"{self.get_room_object_map_name(mastery)} - Mastery"
38 self.location_id_to_name[mastery.ap_id] = location_name 45 self.location_id_to_name[mastery.ap_id] = location_name
46 self.location_name_groups.setdefault("Masteries", []).append(location_name)
39 47
40 for ending in self.objects.endings: 48 for ending in self.objects.endings:
41 location_name = f"{self.get_room_object_map_name(ending)} - {ending.name.title()} Ending" 49 location_name = f"{self.get_room_object_map_name(ending)} - {ending.name.title()} Ending"
42 self.location_id_to_name[ending.ap_id] = location_name 50 self.location_id_to_name[ending.ap_id] = location_name
51 self.location_name_groups.setdefault("Endings", []).append(location_name)
43 52
44 for progressive in self.objects.progressives: 53 for progressive in self.objects.progressives:
45 self.item_id_to_name[progressive.ap_id] = progressive.name 54 self.item_id_to_name[progressive.ap_id] = progressive.name
46 55
56 for door_group in self.objects.door_groups:
57 self.item_id_to_name[door_group.ap_id] = door_group.name
58
47 for keyholder in self.objects.keyholders: 59 for keyholder in self.objects.keyholders:
48 if keyholder.HasField("key"): 60 if keyholder.HasField("key"):
49 location_name = f"{self.get_room_object_location_prefix(keyholder)} - {keyholder.key.upper()} Keyholder" 61 location_name = f"{self.get_room_object_location_prefix(keyholder)} - {keyholder.key.upper()} Keyholder"
50 self.location_id_to_name[keyholder.ap_id] = location_name 62 self.location_id_to_name[keyholder.ap_id] = location_name
63 self.location_name_groups.setdefault("Keyholders", []).append(location_name)
51 64
52 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" 65 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done"
53 66