""" Archipelago init file for Lingo 2 """ import asyncio import os.path import pkgutil import subprocess from typing import ClassVar import Utils import settings from BaseClasses import ItemClassification, Item, Tutorial from settings import Group, UserFilePath from worlds.AutoWorld import WebWorld, World from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS from .options import Lingo2Options from .player_logic import Lingo2PlayerLogic from .regions import create_regions, shuffle_entrances, connect_ports_from_ut from .static_logic import Lingo2StaticLogic from .version import APWORLD_VERSION from ..LauncherComponents import Component, Type, components class Lingo2WebWorld(WebWorld): rich_text_options_doc = True theme = "grass" tutorials = [Tutorial( "Multiworld Setup Guide", "A guide to playing Lingo 2 with Archipelago.", "English", "en_Lingo_2.md", "setup/en", ["hatkirby"] )] class Lingo2Settings(Group): class ExecutableFile(UserFilePath): """Path to the Lingo 2 executable""" is_exe = True exe_file: ExecutableFile = ExecutableFile() class Lingo2World(World): """ Lingo 2 is a first person indie puzzle game where you solve word puzzles in a labyrinthe world. Compared to its predecessor, Lingo 2 has new mechanics, more areas, and a unique progression system where you have to unlock letters before using them in puzzle solutions. """ game = "Lingo 2" web = Lingo2WebWorld() settings: ClassVar[Lingo2Settings] settings_key = "lingo2_options" topology_present = True options_dataclass = Lingo2Options options: Lingo2Options static_logic = Lingo2StaticLogic() item_name_to_id = static_logic.item_name_to_id location_name_to_id = static_logic.location_name_to_id item_name_groups = static_logic.item_name_groups location_name_groups = static_logic.location_name_groups player_logic: Lingo2PlayerLogic port_pairings: dict[int, int] def generate_early(self): self.player_logic = Lingo2PlayerLogic(self) self.port_pairings = {} def create_regions(self): create_regions(self) def connect_entrances(self): if self.options.shuffle_worldports: if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] self.port_pairings = {int(fp): int(tp) for fp, tp in slot_value.items()} connect_ports_from_ut(self.port_pairings, self) else: shuffle_entrances(self) from Utils import visualize_regions visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") def create_items(self): pool = [self.create_item(name) for name in self.player_logic.real_items] total_locations = sum(len(locs) for locs in self.player_logic.locations_by_room.values()) item_difference = total_locations - len(pool) if self.options.trap_percentage > 0: num_traps = int(item_difference * self.options.trap_percentage / 100) item_difference = item_difference - num_traps trap_names = [] trap_weights = [] for letter_name, weight in self.static_logic.letter_weights.items(): trap_names.append(f"Anti {letter_name}") trap_weights.append(weight) bad_letters = self.random.choices(trap_names, weights=trap_weights, k=num_traps) pool += [self.create_item(trap_name) for trap_name in bad_letters] for i in range(0, item_difference): pool.append(self.create_item(self.get_filler_item_name())) self.multiworld.itempool += pool def create_item(self, name: str) -> Item: return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else ItemClassification.progression, self.item_name_to_id.get(name), self.player) def set_rules(self): self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) def fill_slot_data(self): slot_options = [ "cyan_door_behavior", "daedalus_roof_access", "keyholder_sanity", "shuffle_control_center_colors", "shuffle_doors", "shuffle_gallery_paintings", "shuffle_letters", "shuffle_symbols", "shuffle_worldports", "strict_cyan_ending", "strict_purple_ending", "victory_condition", ] slot_data: dict[str, object] = { **self.options.as_dict(*slot_options), "version": [self.static_logic.get_data_version(), APWORLD_VERSION], } if self.options.shuffle_worldports: slot_data["port_pairings"] = self.port_pairings return slot_data def get_filler_item_name(self) -> str: return "A Job Well Done" # for the universal tracker, doesn't get called in standard gen # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md @staticmethod def interpret_slot_data(slot_data: dict[str, object]) -> dict[str, object]: # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough # we are using re_gen_passthrough over modifying the world here due to complexities with ER return slot_data async def run_game(): exe_file = settings.get_settings().lingo2_options.exe_file if Lingo2World.zip_path is not None: # This is a packaged apworld. init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") init_path = Utils.local_path("data", "lingo2_init.tscn") with open(init_path, "wb") as file_handle: file_handle.write(init_scene) subprocess.Popen( [ exe_file, "--scene", init_path, "--", str(Lingo2World.zip_path.absolute()), ], cwd=os.path.dirname(exe_file), ) else: # The world is unzipped and being run in source. subprocess.Popen( [ exe_file, "--scene", Utils.local_path("worlds", "lingo2", "client", "run_from_source.tscn"), "--", Utils.local_path("worlds", "lingo2", "client"), ], cwd=os.path.dirname(exe_file), ) async def client_main(): Utils.async_start(run_game()) def launch_client(*args): asyncio.run(client_main()) component = Component("Lingo 2 Client", component_type=Type.CLIENT, func=launch_client, description="Open Lingo 2.") components.append(component)