about summary refs log tree commit diff stats
path: root/data/maps/the_liberated/metadata.txtpb
blob: a92d7e53f904db4db0c0af80a59d52ef06d4ba4b (plain) (blame)
1
from typing import TYPE_CHECKING, Iterator

from BaseClasses import MultiWorld, CollectionState, ItemClassification, Region, Entrance
from NetUtils import NetworkItem
from . import Lingo2World, Lingo2Item
from .regions import connect_ports_from_ut
from .options import Lingo2Options, ShuffleLetters

if TYPE_CHECKING:
    from .context import Lingo2Manager

PLAYER_NUM = 1


class Tracker:
    manager: "Lingo2Manager"

    multiworld: MultiWorld
    world: Lingo2World

    collected_items: dict[int, int]
    checked_locations: set[int]
    accessible_locations: set[int]
    accessible_worldports: set[int]
    goal_accessible: bool

    state: CollectionState

    def __init__(self, manager: "Lingo2Manager"):
        self.manager = manager
        self.collected_items = {}
        self.checked_locations = set()
        self.accessible_locations = set()
        self.accessible_worldports = set()
        self.goal_accessible = False

    def setup_slot(self, slot_data):
        Lingo2World.for_tracker = True

        self.multiworld = MultiWorld(players=PLAYER_NUM)
        self.world = Lingo2World(self.multiworld, PLAYER_NUM)
        self.multiworld.worlds[1] = self.world
        self.world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default))
                                              for k, t in Lingo2Options.type_hints.items()})

        self.world.generate_early()
        self.world.create_regions()

        if self.world.options.shuffle_worldports:
            port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()}
            connect_ports_from_ut(port_pairings, self.world)

        self.refresh_state()

    def set_checked_locations(self, checked_locations: set[int]):
        self.checked_locations = checked_locations.copy()

    def set_collected_items(self, network_items: list[NetworkItem]):
        self.collected_items = {}

        for item in network_items:
            self.collected_items[item.item] = self.collected_items.get(item.item, 0) + 1

        self.refresh_state()

    def refresh_state(self):
        self.state = CollectionState(self.multiworld)

        for item_id, item_amount in self.collected_items.items():
            for i in range(item_amount):
                self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id),
                                              ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True)

        for k, v in self.manager.keyboard.items():
            # Unless all level 1 letters are pre-unlocked, H1 I1 N1 and T1 act differently between the generator and
            # game. The generator considers them to be unlocked, which means they are not included in logic
            # requirements, and only one item/event is needed to unlock their level 2 forms. The game considers them to
            # be vanilla, which means you still have to pick them up in the Starting Room in order for them to appear on
            # your keyboard. This also means that whether or not you have the level 1 forms should be synced to the
            # multiworld. The tracker specifically should collect one fewer item for these letters in this scenario.
            tv = v
            if k in "hint" and self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla,
                                                                      ShuffleLetters.option_progressive]:
                tv = max(0, v - 1)

            if tv > 0:
                for i in range(tv):
                    self.state.collect(Lingo2Item(k.upper(), ItemClassification.progression, None, PLAYER_NUM),
                                       prevent_sweep=True)

        for port_id in self.manager.worldports:
            self.state.collect(Lingo2Item(f"Worldport {port_id} Entered", ItemClassification.progression, None,
                                          PLAYER_NUM), prevent_sweep=True)

        self.state.sweep_for_advancements()

        self.accessible_locations = set()
        self.accessible_worldports = set()
        self.goal_accessible = False

        for region in self.state.reachable_regions[PLAYER_NUM]:
            for location in region.locations:
                if location.access_rule(self.state):
                    if location.address is not None:
                        if location.address not in self.checked_locations:
                            self.accessible_locations.add(location.address)
                    elif hasattr(location, "port_id"):
                        if location.port_id not in self.manager.worldports:
                            self.accessible_worldports.add(location.port_id)
                    elif hasattr(location, "goal") and location.goal:
                        if not self.manager.goaled:
                            self.goal_accessible = True

    def get_path_to_location(self, location_id: int) -> list[str] | None:
        location_name = self.world.location_id_to_name.get(location_id)
        location = self.multiworld.get_location(location_name, PLAYER_NUM)
        return self.get_logical_path(location.parent_region)

    def get_path_to_port(self, port_id: int) -> list[str] | None:
        port = self.world.static_logic.objects.ports[port_id]
        region_name = self.world.static_logic.get_room_region_name(port.room_id)
        region = self.multiworld.get_region(region_name, PLAYER_NUM)
        return self.get_logical_path(region)

    def get_path_to_goal(self):
        room_id = self.world.player_logic.goal_room_id
        region_name = self.world.static_logic.get_room_region_name(room_id)
        region = self.multiworld.get_region(region_name, PLAYER_NUM)
        return self.get_logical_path(region)

    def get_logical_path(self, region: Region) -> list[str] | None:
        if region not in self.state.path:
            return None

        def flist_to_iter(path_value) -> Iterator[str]:
            while path_value:
                region_or_entrance, path_value = path_value
                yield region_or_entrance

        reversed_path = self.state.path.get(region)
        flat_path = reversed(list(map(str, flist_to_iter(reversed_path))))

        return list(flat_path)[1::2]