diff options
Diffstat (limited to 'apworld/__init__.py')
| -rw-r--r-- | apworld/__init__.py | 105 |
1 files changed, 97 insertions, 8 deletions
| diff --git a/apworld/__init__.py b/apworld/__init__.py index f1de503..6b5338e 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py | |||
| @@ -1,14 +1,18 @@ | |||
| 1 | """ | 1 | """ |
| 2 | Archipelago init file for Lingo 2 | 2 | Archipelago init file for Lingo 2 |
| 3 | """ | 3 | """ |
| 4 | from typing import ClassVar | ||
| 5 | |||
| 4 | from BaseClasses import ItemClassification, Item, Tutorial | 6 | from BaseClasses import ItemClassification, Item, Tutorial |
| 7 | from Options import OptionError | ||
| 8 | from settings import Group, UserFilePath | ||
| 5 | from worlds.AutoWorld import WebWorld, World | 9 | from worlds.AutoWorld import WebWorld, World |
| 6 | from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS | 10 | from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS, ALL_LETTERS_UPPER |
| 7 | from .options import Lingo2Options | 11 | from .options import Lingo2Options |
| 8 | from .player_logic import Lingo2PlayerLogic | 12 | from .player_logic import Lingo2PlayerLogic |
| 9 | from .regions import create_regions | 13 | from .regions import create_regions, shuffle_entrances, connect_ports_from_ut |
| 10 | from .static_logic import Lingo2StaticLogic | 14 | from .static_logic import Lingo2StaticLogic |
| 11 | from .version import APWORLD_VERSION | 15 | from worlds.LauncherComponents import Component, Type, components, launch as launch_component, icon_paths |
| 12 | 16 | ||
| 13 | 17 | ||
| 14 | class Lingo2WebWorld(WebWorld): | 18 | class Lingo2WebWorld(WebWorld): |
| @@ -24,6 +28,15 @@ class Lingo2WebWorld(WebWorld): | |||
| 24 | )] | 28 | )] |
| 25 | 29 | ||
| 26 | 30 | ||
| 31 | class Lingo2Settings(Group): | ||
| 32 | class ExecutableFile(UserFilePath): | ||
| 33 | """Path to the Lingo 2 executable""" | ||
| 34 | is_exe = True | ||
| 35 | |||
| 36 | exe_file: ExecutableFile = ExecutableFile() | ||
| 37 | start_game: bool = True | ||
| 38 | |||
| 39 | |||
| 27 | class Lingo2World(World): | 40 | class Lingo2World(World): |
| 28 | """ | 41 | """ |
| 29 | Lingo 2 is a first person indie puzzle game where you solve word puzzles in a labyrinthe world. Compared to its | 42 | Lingo 2 is a first person indie puzzle game where you solve word puzzles in a labyrinthe world. Compared to its |
| @@ -33,6 +46,9 @@ class Lingo2World(World): | |||
| 33 | game = "Lingo 2" | 46 | game = "Lingo 2" |
| 34 | web = Lingo2WebWorld() | 47 | web = Lingo2WebWorld() |
| 35 | 48 | ||
| 49 | settings: ClassVar[Lingo2Settings] | ||
| 50 | settings_key = "lingo2_options" | ||
| 51 | |||
| 36 | topology_present = True | 52 | topology_present = True |
| 37 | 53 | ||
| 38 | options_dataclass = Lingo2Options | 54 | options_dataclass = Lingo2Options |
| @@ -44,17 +60,42 @@ class Lingo2World(World): | |||
| 44 | item_name_groups = static_logic.item_name_groups | 60 | item_name_groups = static_logic.item_name_groups |
| 45 | location_name_groups = static_logic.location_name_groups | 61 | location_name_groups = static_logic.location_name_groups |
| 46 | 62 | ||
| 63 | for_tracker: ClassVar[bool] = False | ||
| 64 | |||
| 47 | player_logic: Lingo2PlayerLogic | 65 | player_logic: Lingo2PlayerLogic |
| 48 | 66 | ||
| 67 | port_pairings: dict[int, int] | ||
| 68 | |||
| 49 | def generate_early(self): | 69 | def generate_early(self): |
| 50 | self.player_logic = Lingo2PlayerLogic(self) | 70 | self.player_logic = Lingo2PlayerLogic(self) |
| 71 | self.port_pairings = {} | ||
| 72 | |||
| 73 | if self.options.restrict_letter_placements: | ||
| 74 | self.options.local_items.value |= set(ALL_LETTERS_UPPER) | ||
| 51 | 75 | ||
| 52 | def create_regions(self): | 76 | def create_regions(self): |
| 77 | if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: | ||
| 78 | self.player_logic.rte_mapping = [self.world.static_logic.map_id_by_name[map_name] | ||
| 79 | for map_name in self.multiworld.re_gen_passthrough["Lingo 2"]["rte"]] | ||
| 80 | |||
| 53 | create_regions(self) | 81 | create_regions(self) |
| 54 | 82 | ||
| 55 | from Utils import visualize_regions | 83 | def connect_entrances(self): |
| 84 | if self.options.shuffle_worldports: | ||
| 85 | if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: | ||
| 86 | slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] | ||
| 87 | self.port_pairings = { | ||
| 88 | self.static_logic.port_id_by_ap_id[int(fp)]: self.static_logic.port_id_by_ap_id[int(tp)] | ||
| 89 | for fp, tp in slot_value.items() | ||
| 90 | } | ||
| 91 | |||
| 92 | connect_ports_from_ut(self.port_pairings, self) | ||
| 93 | else: | ||
| 94 | shuffle_entrances(self) | ||
| 95 | |||
| 96 | #from Utils import visualize_regions | ||
| 56 | 97 | ||
| 57 | visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") | 98 | #visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") |
| 58 | 99 | ||
| 59 | def create_items(self): | 100 | def create_items(self): |
| 60 | pool = [self.create_item(name) for name in self.player_logic.real_items] | 101 | pool = [self.create_item(name) for name in self.player_logic.real_items] |
| @@ -79,38 +120,86 @@ class Lingo2World(World): | |||
| 79 | for i in range(0, item_difference): | 120 | for i in range(0, item_difference): |
| 80 | pool.append(self.create_item(self.get_filler_item_name())) | 121 | pool.append(self.create_item(self.get_filler_item_name())) |
| 81 | 122 | ||
| 123 | if not any(ItemClassification.progression in item.classification for item in pool): | ||
| 124 | raise OptionError(f"Lingo 2 player {self.player} has no progression items. Please enable at least one " | ||
| 125 | f"option that would add progression gating to your world, such as Shuffle Doors or " | ||
| 126 | f"Shuffle Letters.") | ||
| 127 | |||
| 82 | self.multiworld.itempool += pool | 128 | self.multiworld.itempool += pool |
| 83 | 129 | ||
| 130 | for name in self.player_logic.starting_items: | ||
| 131 | self.push_precollected(self.create_item(name)) | ||
| 132 | |||
| 84 | def create_item(self, name: str) -> Item: | 133 | def create_item(self, name: str) -> Item: |
| 85 | return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else | 134 | item = Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else |
| 86 | ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else | 135 | ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else |
| 87 | ItemClassification.progression, | 136 | ItemClassification.progression, |
| 88 | self.item_name_to_id.get(name), self.player) | 137 | self.item_name_to_id.get(name), self.player) |
| 89 | 138 | ||
| 139 | item.is_letter = (name in ALL_LETTERS_UPPER) | ||
| 140 | return item | ||
| 141 | |||
| 90 | def set_rules(self): | 142 | def set_rules(self): |
| 91 | self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) | 143 | self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) |
| 92 | 144 | ||
| 93 | def fill_slot_data(self): | 145 | def fill_slot_data(self): |
| 94 | slot_options = [ | 146 | slot_options = [ |
| 95 | "cyan_door_behavior", | 147 | "cyan_door_behavior", |
| 148 | "daedalus_only", | ||
| 96 | "daedalus_roof_access", | 149 | "daedalus_roof_access", |
| 150 | "enable_gift_maps", | ||
| 151 | "enable_icarus", | ||
| 152 | "endings_requirement", | ||
| 153 | "fast_travel_access", | ||
| 97 | "keyholder_sanity", | 154 | "keyholder_sanity", |
| 155 | "masteries_requirement", | ||
| 98 | "shuffle_control_center_colors", | 156 | "shuffle_control_center_colors", |
| 99 | "shuffle_doors", | 157 | "shuffle_doors", |
| 100 | "shuffle_gallery_paintings", | 158 | "shuffle_gallery_paintings", |
| 101 | "shuffle_letters", | 159 | "shuffle_letters", |
| 160 | "shuffle_music", | ||
| 102 | "shuffle_symbols", | 161 | "shuffle_symbols", |
| 162 | "shuffle_worldports", | ||
| 103 | "strict_cyan_ending", | 163 | "strict_cyan_ending", |
| 104 | "strict_purple_ending", | 164 | "strict_purple_ending", |
| 105 | "victory_condition", | 165 | "victory_condition", |
| 106 | ] | 166 | ] |
| 107 | 167 | ||
| 108 | slot_data = { | 168 | slot_data: dict[str, object] = { |
| 109 | **self.options.as_dict(*slot_options), | 169 | **self.options.as_dict(*slot_options), |
| 110 | "version": [self.static_logic.get_data_version(), APWORLD_VERSION], | 170 | "custom_mint_ending": self.player_logic.custom_mint_ending or "", |
| 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), | ||
| 173 | "version": self.static_logic.get_data_version(), | ||
| 111 | } | 174 | } |
| 112 | 175 | ||
| 176 | if self.options.shuffle_worldports: | ||
| 177 | def get_port_ap_id(port_id): | ||
| 178 | return self.static_logic.objects.ports[port_id].ap_id | ||
| 179 | |||
| 180 | slot_data["port_pairings"] = {get_port_ap_id(from_id): get_port_ap_id(to_id) | ||
| 181 | for from_id, to_id in self.port_pairings.items()} | ||
| 182 | |||
| 113 | return slot_data | 183 | return slot_data |
| 114 | 184 | ||
| 115 | def get_filler_item_name(self) -> str: | 185 | def get_filler_item_name(self) -> str: |
| 116 | return "A Job Well Done" | 186 | return "A Job Well Done" |
| 187 | |||
| 188 | # for the universal tracker, doesn't get called in standard gen | ||
| 189 | # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md | ||
| 190 | @staticmethod | ||
| 191 | def interpret_slot_data(slot_data: dict[str, object]) -> dict[str, object]: | ||
| 192 | # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough | ||
| 193 | # we are using re_gen_passthrough over modifying the world here due to complexities with ER | ||
| 194 | return slot_data | ||
| 195 | |||
| 196 | |||
| 197 | def launch_client(*args): | ||
| 198 | from .context import client_main | ||
| 199 | launch_component(client_main, name="Lingo2Client", args=args) | ||
| 200 | |||
| 201 | |||
| 202 | icon_paths["lingo2_ico"] = f"ap:{__name__}/logo.png" | ||
| 203 | component = Component("Lingo 2 Client", component_type=Type.CLIENT, func=launch_client, | ||
| 204 | description="Open Lingo 2.", supports_uri=True, game_name="Lingo 2", icon="lingo2_ico") | ||
| 205 | components.append(component) | ||
