about summary refs log tree commit diff stats
path: root/data/maps/the_relentless
Commit message (Collapse)AuthorAgeFilesLines
* (Almost) all panels are locations or connections nowStar Rauchenberger2025-11-011-2/+4
|
* Added backwards compatibility with removed locationsStar Rauchenberger2025-10-261-2/+4
|
* Changed the_relentless's locationsStar Rauchenberger2025-10-251-4/+28
|
* Changed how door location names are formattedStar Rauchenberger2025-08-307-7/+0
| | | | | | | | | | | | | | | | | | STANDARD type doors with at most four panels in the same map area and no other trigger objects will have their location names generated from the names of the panels used to open the door, similar to Lingo 1. Other door types will use the door's name. In either case, the name can be overridden using the new location_name field. Rooms can also set a panel_display_name field, which will be used in location names for doors, and is used to group panels into areas. Panels themselves can set display names, which differentiates their locations from other panels in the same area. Many maps were updated for this, but note that the_symbolic and the_unyielding have validator failures because of duplicate panel names. This won't matter until panelsanity is implemented.
* Converted puzzle symbols to an enumStar Rauchenberger2025-08-206-30/+30
|
* Maps have display names nowStar Rauchenberger2025-08-201-0/+1
| | | | Also added endings to the apworld.
* Added the_relentlessStar Rauchenberger2025-08-1810-0/+409
4'>184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
from typing import TYPE_CHECKING

import BaseClasses
from BaseClasses import Region, ItemClassification, Entrance
from entrance_rando import randomize_entrances
from .items import Lingo2Item
from .locations import Lingo2Location
from .player_logic import AccessRequirements
from .rules import make_location_lambda

if TYPE_CHECKING:
    from . import Lingo2World


def create_region(room, world: "Lingo2World") -> Region:
    return Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld)


def create_locations(room, new_region: Region, world: "Lingo2World", regions: dict[str, Region]):
    for location in world.player_logic.locations_by_room.get(room.id, {}):
        reqs = location.reqs.copy()
        reqs.remove_room(new_region.name)

        new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code],
                                      location.code, new_region)
        new_location.access_rule = make_location_lambda(reqs, world, regions)
        new_region.locations.append(new_location)

    for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items():
        new_location = Lingo2Location(world.player, event_name, None, new_region)
        if world.for_tracker and item_name == "Victory":
            new_location.goal = True

        event_item = Lingo2Item(item_name, ItemClassification.progression, None, world.player)
        new_location.place_locked_item(event_item)
        new_region.locations.append(new_location)

    if world.for_tracker and world.options.shuffle_worldports:
        for port_id in room.ports:
            port = world.static_logic.objects.ports[port_id]
            if port.no_shuffle:
                continue

            new_location = Lingo2Location(world.player, f"Worldport {port.id} Entered", None, new_region)
            new_location.port_id = port.id

            if port.HasField("required_door"):
                new_location.access_rule = \
                    make_location_lambda(world.player_logic.get_door_open_reqs(port.required_door), world, regions)

            new_region.locations.append(new_location)


def create_regions(world: "Lingo2World"):
    regions = {
        "Menu": Region("Menu", world.player, world.multiworld)
    }

    region_and_room = []

    # Create the regions in two stages. First, make the actual region objects and memoize them. Then, add all of the
    # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
    # faster than having to look them up during access checking.
    for room in world.static_logic.objects.rooms:
        if room.map_id not in world.player_logic.shuffled_maps:
            continue

        region = create_region(room, world)
        regions[region.name] = region
        region_and_room.append((region, room))

    for (region, room) in region_and_room:
        create_locations(room, region, world, regions)

    regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game")

    for connection in world.static_logic.objects.connections:
        if connection.roof_access and not world.options.daedalus_roof_access:
            continue

        if connection.vanilla_only and world.options.shuffle_doors:
            continue

        from_region = world.static_logic.get_room_region_name(connection.from_room)
        to_region = world.static_logic.get_room_region_name(connection.to_room)

        if from_region not in regions or to_region not in regions:
            continue

        connection_name = f"{from_region} -> {to_region}"

        reqs = AccessRequirements()

        if connection.HasField("required_door"):
            reqs.merge(world.player_logic.get_door_open_reqs(connection.required_door))

            door = world.static_logic.objects.doors[connection.required_door]
            wmap = world.static_logic.objects.maps[door.map_id]
            connection_name = f"{connection_name} (using {wmap.name} - {door.name})"

        if connection.HasField("port"):
            port = world.static_logic.objects.ports[connection.port]
            connection_name = f"{connection_name} (via {port.display_name})"

            if world.options.shuffle_worldports and not port.no_shuffle:
                continue

            if port.HasField("required_door"):
                reqs.merge(world.player_logic.get_door_open_reqs(port.required_door))

        if connection.HasField("painting"):
            painting = world.static_logic.objects.paintings[connection.painting]
            connection_name = f"{connection_name} (via painting {painting.name})"

            if painting.HasField("required_door"):
                reqs.merge(world.player_logic.get_door_open_reqs(painting.required_door))

        if connection.HasField("panel"):
            proxy = connection.panel
            reqs.merge(world.player_logic.get_panel_reqs(proxy.panel,
                                                         proxy.answer if proxy.HasField("answer") else None))

            panel = world.static_logic.objects.panels[proxy.panel]
            if proxy.HasField("answer"):
                connection_name = f"{connection_name} (via panel {panel.name}/{proxy.answer})"
            else:
                connection_name = f"{connection_name} (via panel {panel.name})"

        if connection.HasField("purple_ending") and connection.purple_ending and world.options.strict_purple_ending:
            world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyz")

        if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending:
            world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")

        reqs.simplify()
        reqs.remove_room(from_region)

        if to_region in reqs.rooms:
            # This connection can't ever increase access because you're required to have access to the other side in
            # order for it to be usable. We will just not create the connection at all, in order to help GER figure out
            # what regions are dead ends.
            continue

        connection = Entrance(world.player, connection_name, regions[from_region])
        connection.access_rule = make_location_lambda(reqs, world, regions)

        regions[from_region].exits.append(connection)
        connection.connect(regions[to_region])

        for region in reqs.get_referenced_rooms():
            world.multiworld.register_indirect_condition(regions[region], connection)

    world.multiworld.regions += regions.values()


def shuffle_entrances(world: "Lingo2World"):
    er_entrances: list[Entrance] = []
    er_exits: list[Entrance] = []

    port_id_by_name: dict[str, int] = {}

    shuffleable_ports = [port for port in world.static_logic.objects.ports
                         if not port.no_shuffle
                         and world.static_logic.get_room_object_map_id(port) in world.player_logic.shuffled_maps]

    if len(shuffleable_ports) % 2 == 1:
        # We have an odd number of shuffleable ports! Pick a port from a room that has more than one, and make it a
        # redundant warp to another port.
        redundant_rooms = set(room.id for room in world.static_logic.objects.rooms if len(room.ports) > 1)
        redundant_ports = [port for port in shuffleable_ports if port.room_id in redundant_rooms]
        chosen_port = world.random.choice(redundant_ports)

        shuffleable_ports.remove(chosen_port)

        chosen_destination = world.random.choice(shuffleable_ports)

        world.port_pairings[chosen_port.id] = chosen_destination.id

        from_region_name = world.static_logic.get_room_region_name(chosen_port.room_id)
        to_region_name = world.static_logic.get_room_region_name(chosen_destination.room_id)

        from_region = world.multiworld.get_region(from_region_name, world.player)
        to_region = world.multiworld.get_region(to_region_name, world.player)

        connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region)
        from_region.exits.append(connection)
        connection.connect(to_region)

        if chosen_port.HasField("required_door"):
            door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door)
            connection.access_rule = make_location_lambda(door_reqs, world, None)

            for region in door_reqs.get_referenced_rooms():
                world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
                                                             connection)

    for port in shuffleable_ports:
        port_region_name = world.static_logic.get_room_region_name(port.room_id)
        port_region = world.multiworld.get_region(port_region_name, world.player)

        connection_name = f"{port_region_name} - {port.display_name}"
        port_id_by_name[connection_name] = port.id

        entrance = port_region.create_er_target(connection_name)
        entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY

        er_exit = port_region.create_exit(connection_name)
        er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY

        if port.HasField("required_door"):
            door_reqs = world.player_logic.get_door_open_reqs(port.required_door)
            er_exit.access_rule = make_location_lambda(door_reqs, world, None)

            for region in door_reqs.get_referenced_rooms():
                world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
                                                             er_exit)

        er_entrances.append(entrance)
        er_exits.append(er_exit)

    result = randomize_entrances(world, True, {0:[0]}, False, er_entrances,
                                 er_exits)

    for (f, to) in result.pairings:
        world.port_pairings[port_id_by_name[f]] = port_id_by_name[to]


def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"):
    for fpid, tpid in port_pairings.items():
        from_port = world.static_logic.objects.ports[fpid]
        to_port = world.static_logic.objects.ports[tpid]

        from_region_name = world.static_logic.get_room_region_name(from_port.room_id)
        to_region_name = world.static_logic.get_room_region_name(to_port.room_id)

        from_region = world.multiworld.get_region(from_region_name, world.player)
        to_region = world.multiworld.get_region(to_region_name, world.player)

        connection = Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region)

        reqs = AccessRequirements()
        if from_port.HasField("required_door"):
            reqs = world.player_logic.get_door_open_reqs(from_port.required_door).copy()

        if world.for_tracker:
            reqs.items.add(f"Worldport {fpid} Entered")

        if not reqs.is_empty():
            connection.access_rule = make_location_lambda(reqs, world, None)

            for region in reqs.get_referenced_rooms():
                world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
                                                             connection)

        from_region.exits.append(connection)
        connection.connect(to_region)