def test_multi_create_pickup_data_for_other(pickup_for_create_pickup_data):
    # Setup
    multi = ItemResourceInfo("Multiworld", "Multiworld", 30, None)
    solo = pickup_exporter.PickupExporterSolo(pickup_exporter.GenericAcquiredMemo())
    creator = pickup_exporter.PickupExporterMulti(solo, multi, PlayersConfiguration(0, {0: "You", 1: "Someone"}))
    model = MagicMock()
    resource_a = ItemResourceInfo("A", "A", 10, None)
    resource_b = ItemResourceInfo("B", "B", 10, None)

    # Run
    data = creator.create_details(PickupIndex(10), PickupTarget(pickup_for_create_pickup_data, 1),
                                  pickup_for_create_pickup_data, PickupModelStyle.ALL_VISIBLE,
                                  "Scan Text", model)

    # Assert
    assert data == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(10),
        scan_text="Someone's Scan Text",
        hud_text=['Sent Cake to Someone!'],
        conditional_resources=[
            ConditionalResources(None, None, ((multi, 11),)),
        ],
        conversion=[],
        model=model,
        other_player=True,
        original_pickup=pickup_for_create_pickup_data,
    )
def test_multi_create_pickup_data_for_self(pickup_for_create_pickup_data):
    # Setup
    solo = pickup_exporter.PickupExporterSolo(pickup_exporter.GenericAcquiredMemo())
    creator = pickup_exporter.PickupExporterMulti(solo, MagicMock(), PlayersConfiguration(0, {0: "You", 1: "Someone"}))
    model = MagicMock()
    resource_a = ItemResourceInfo(0, "A", "A", 10)
    resource_b = ItemResourceInfo(1, "B", "B", 10)

    # Run
    data = creator.create_details(PickupIndex(10), PickupTarget(pickup_for_create_pickup_data, 0),
                                  pickup_for_create_pickup_data, PickupModelStyle.ALL_VISIBLE,
                                  "Scan Text", model)

    # Assert
    assert data == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(10),
        scan_text="Your Scan Text",
        hud_text=['A acquired!', 'B acquired!'],
        conditional_resources=[
            ConditionalResources("A", None, ((resource_a, 1),)),
            ConditionalResources("B", resource_a, ((resource_b, 1),)),
        ],
        conversion=[],
        model=model,
        other_player=False,
        original_pickup=pickup_for_create_pickup_data,
    )
Esempio n. 3
0
def _simplified_memo_data() -> Dict[str, str]:
    result = pickup_exporter.GenericAcquiredMemo()
    result["Locked Power Bomb Expansion"] = (
        "Power Bomb Expansion acquired, "
        "but the main Power Bomb is required to use it.")
    result[
        "Locked Missile Expansion"] = "Missile Expansion acquired, but the Missile Launcher is required to use it."
    result[
        "Locked Seeker Launcher"] = "Seeker Launcher acquired, but the Missile Launcher is required to use it."
    return result
    def create_data(self) -> dict:
        db = self.game
        useless_target = PickupTarget(pickup_creator.create_nothing_pickup(db.resource_database),
                                      self.players_config.player_index)

        pickup_list = pickup_exporter.export_all_indices(
            self.patches,
            useless_target,
            db.world_list,
            self.rng,
            self.configuration.pickup_model_style,
            self.configuration.pickup_model_data_source,
            exporter=pickup_exporter.create_pickup_exporter(db, pickup_exporter.GenericAcquiredMemo(),
                                                            self.players_config),
            visual_etm=pickup_creator.create_visual_etm(),
        )

        gameplay_patch_list = [field.name for field in dataclasses.fields(self.configuration.patches)]
        cosmetic_patch_list = [field.name for field in dataclasses.fields(self.cosmetic_patches)]

        cosmetic_patch_list.remove("music")
        specific_patches = {}

        for patch in gameplay_patch_list:
            specific_patches[patch] = getattr(self.configuration.patches, patch)
        for patch in cosmetic_patch_list:
            specific_patches[patch] = getattr(self.cosmetic_patches, patch)
        if self.cosmetic_patches.music != MusicMode.VANILLA:
            specific_patches[self.cosmetic_patches.music.value] = True

        starting_point = self.patches.starting_location

        starting_area = db.world_list.area_by_area_location(starting_point)

        starting_save_index = starting_area.extra["save_index"]

        starting_location_info = {
            "starting_region": starting_point.world_name,
            "starting_save_station_index": starting_save_index,
        }

        return {
            "pickups": [
                sm_pickup_details_to_patcher(detail)
                for detail in pickup_list
            ],
            "starting_items": [
                sm_starting_items_to_patcher(item, qty)
                for item, qty in self.patches.starting_items.as_resource_gain()
            ],
            "specific_patches": specific_patches,
            "starting_conditions": starting_location_info
        }
Esempio n. 5
0
    def create_data(self) -> dict:
        db = self.game
        namer = PrimeHintNamer(self.description.all_patches, self.players_config)

        scan_visor = self.game.resource_database.get_item_by_name("Scan Visor")
        useless_target = PickupTarget(pickup_creator.create_nothing_pickup(db.resource_database),
                                      self.players_config.player_index)

        pickup_list = pickup_exporter.export_all_indices(
            self.patches,
            useless_target,
            db.world_list,
            self.rng,
            self.configuration.pickup_model_style,
            self.configuration.pickup_model_data_source,
            exporter=pickup_exporter.create_pickup_exporter(db, pickup_exporter.GenericAcquiredMemo(),
                                                            self.players_config),
            visual_etm=pickup_creator.create_visual_etm(),
        )
        modal_hud_override = _create_locations_with_modal_hud_memo(pickup_list)

        world_data = {}

        for world in db.world_list.worlds:
            if world.name == "End of Game":
                continue

            world_data[world.name] = {
                "transports": {},
                "rooms": {}
            }

            for area in world.areas:
                world_data[world.name]["rooms"][area.name] = {"pickups":[]}

                def node_len(a: PickupNode):
                    return a.pickup_index

                pickup_nodes = sorted((node for node in area.nodes if isinstance(node, PickupNode)), key=node_len)
                for node in pickup_nodes:
                    pickup_index = node.pickup_index.index
                    pickup = prime1_pickup_details_to_patcher(pickup_list[pickup_index],
                                                              pickup_index in modal_hud_override,
                                                              self.rng)

                    if node.extra.get("position_required"):
                        assert self.configuration.items_every_room
                        aabb = area.extra["aabb"]

                        # return a random float between (min, max) biased towards target (up to 1 re-roll to get closer)
                        def random_factor(rng: Random, min: float, max: float, target: float):
                            a = rng.uniform(min, max)
                            b = rng.uniform(min, max)
                            a_diff = abs(a - target)
                            b_diff = abs(b - target)
                            if a_diff > b_diff:
                                return a
                            return b

                        # return a quasi-random point within the provided aabb, but bias towards being closer to in-bounds
                        def pick_random_point_in_aabb(rng: Random, aabb: list, room_name: str):
                            offset_xy = 0.0
                            offset_max_z = 0.0

                            ROOMS_THAT_NEED_HELP = [
                                "Landing Site",
                                "Alcove",
                                "Frigate Crash Site",
                                "Sunchamber",
                                "Triclops Pit",
                                "Elite Quarters",
                                "Quarantine Cave",
                                "Burn Done",
                                "Reserach Lab Hydra",
                                "Reserach Lab Aether",
                            ]

                            if room_name in ROOMS_THAT_NEED_HELP:
                                offset_xy = 0.1
                                offset_max_z = -0.3

                            x_factor = random_factor(rng, 0.15 + offset_xy, 0.85 - offset_xy, 0.5)
                            y_factor = random_factor(rng, 0.15 + offset_xy, 0.85 - offset_xy, 0.5)
                            z_factor = random_factor(rng, 0.1, 0.8 + offset_max_z, 0.35)

                            return [
                                aabb[0] + (aabb[3]-aabb[0])*x_factor,
                                aabb[1] + (aabb[4]-aabb[1])*y_factor,
                                aabb[2] + (aabb[5]-aabb[2])*z_factor,
                            ]

                        pickup["position"] = pick_random_point_in_aabb(self.rng, aabb, area.name)
                        pickup["jumboScan"] = True # Scan this item through walls

                    world_data[world.name]["rooms"][area.name]["pickups"].append(pickup)

                if self.configuration.superheated_probability != 0:
                    world_data[world.name]["rooms"][area.name]["superheated"] = self.rng.random() < self.configuration.superheated_probability/1000.0

                if self.configuration.submerged_probability != 0:
                    world_data[world.name]["rooms"][area.name]["submerge"] = self.rng.random() < self.configuration.submerged_probability/1000.0

                for node in area.nodes:
                    if not isinstance(node, TeleporterNode) or not node.editable:
                        continue

                    identifier = db.world_list.identifier_for_node(node)
                    target = _name_for_location(db.world_list, self.patches.elevator_connection[identifier])

                    source_name = prime1_elevators.RANDOM_PRIME_CUSTOM_NAMES[(
                        identifier.area_location.world_name,
                        identifier.area_location.area_name,
                    )]
                    world_data[world.name]["transports"][source_name] = target

        if self.configuration.room_rando != RoomRandoMode.NONE:
            for world in db.world_list.worlds:
                if world.name == "End of Game":
                    continue

                area_dock_nums = dict()
                attached_areas = dict()
                size_indices = dict()
                candidates = list()
                default_connections_node_name = dict()
                dock_num_by_area_node = dict()
                is_nonstandard = dict()
                disabled_doors = set()

                for area in world.areas:
                    world_data[world.name]["rooms"][area.name]["doors"] = dict()
                    area_dock_nums[area.name] = list()
                    attached_areas[area.name] = list()
                    for node in area.nodes:
                        if not isinstance(node, DockNode):
                            continue
                        index = node.extra["dock_index"]
                        dock_num_by_area_node[(area.name, node.name)] = index
                        is_nonstandard[(area.name, index)] = node.extra["nonstandard"]
                        default_connections_node_name[(area.name, index)] = (node.default_connection.area_name, node.default_connection.node_name)

                        if node.default_dock_weakness.name == "Permanently Locked":
                            disabled_doors.add((area.name, index))

                        if node.extra["nonstandard"]:
                            continue
                        area_dock_nums[area.name].append(index)
                        attached_areas[area.name].append(node.default_connection.area_name)
                        candidates.append((area.name, index))
                    size_indices[area.name] = area.extra["size_index"]

                default_connections = dict()
                for (src_name, src_dock) in default_connections_node_name:
                    (dst_name, dst_node_name) = default_connections_node_name[(src_name, src_dock)]

                    try:
                        dst_dock = dock_num_by_area_node[(dst_name, dst_node_name)]
                    except KeyError:
                        continue

                    default_connections[(src_name, src_dock)] = (dst_name, dst_dock)

                self.rng.shuffle(candidates)

                used_room_pairings = list()

                def are_rooms_compatible(src_name, src_dock, dst_name, dst_dock, mode: RoomRandoMode):
                    if src_name is None or dst_name is None:
                        # print("none name")
                        return False

                    # both rooms must have patchable docks
                    if len(area_dock_nums[src_name]) == 0 or len(area_dock_nums[dst_name]) == 0:
                        # print("unpatchable room(s)")
                        return False

                    # destinations cannot be in the same room
                    if src_name == dst_name:
                        # print("same room")
                        return False
                    
                    # src/dst must not be exempt
                    if src_dock is not None and is_nonstandard[(src_name, src_dock)]:
                            # print("src exempt")
                            return False
                    if dst_dock is not None and is_nonstandard[(dst_name, dst_dock)]:
                            # print("dst exempt")
                            return False

                    # rooms cannot be neighbors
                    if src_name in attached_areas[dst_name]:
                        if mode == RoomRandoMode.ONE_WAY:
                            # print("neighbor")
                            return False
                        
                        # Unless it's a vanilla 2-way connection
                        if default_connections[(src_name, src_dock)] != (dst_name, dst_dock):
                            # print("two-way non-neighbor")
                            return False

                    # rooms can only connect to another room up to once
                    if {src_name, dst_name} in used_room_pairings:
                        # Except for one-way in impact crater, this edge case works fine and is desireable
                        if not (mode == RoomRandoMode.ONE_WAY and world.name == "Impact Crater"):
                            # print("double connection")
                            return False

                    # The two rooms must not crash if drawn at the same time (size_index > 1.0)
                    if size_indices[src_name] + size_indices[dst_name] >= 1.0:
                        # print("too big")
                        return False

                    return True
                
                if self.configuration.room_rando == RoomRandoMode.ONE_WAY:
                    for area in world.areas:
                        for dock_num in area_dock_nums[area.name]:
                            # First try each of the unused docks
                            dst_name = None
                            dst_dock = None
                            for (name, dock) in candidates:
                                if are_rooms_compatible(area.name, None, name, None, self.configuration.room_rando):
                                    dst_name = name
                                    dst_dock = dock
                                    break

                            # If that wasn't successful, pick random destinations until it works out
                            deadman_count = 1000
                            while dst_name is None or dst_dock is None or not are_rooms_compatible(area.name, dock_num, dst_name, dst_dock, self.configuration.room_rando):

                                deadman_count -= 1
                                if deadman_count == 0:
                                    raise Exception("Failed to find suitible destination for %s:%s" % (area.name, dock_num))

                                dst_name = self.rng.choice(world.areas).name
                                dst_dock = None

                                if len(area_dock_nums[dst_name]) == 0:
                                    continue

                                dst_dock = self.rng.choice(area_dock_nums[dst_name])

                            # Don't use this dock as a destination again unless there are no other options
                            try:
                                candidates.remove((dst_name, dst_dock))
                            except ValueError:
                                # print("re-used %s:%d" % (dst_name, dst_dock))
                                pass

                            used_room_pairings.append({area.name, dst_name})

                            world_data[world.name]["rooms"][area.name]["doors"][str(dock_num)] = {
                                "destination": {
                                    "roomName": dst_name,
                                    "dockNum": dst_dock,
                                }
                            }
                elif self.configuration.room_rando == RoomRandoMode.TWO_WAY:
                    # List containing:
                    #   - set of len=2, each containing
                    #       - tuple of len=2 for (room_name, dock)
                    shuffled = list()

                    def next_candidate(max_index):
                        for (src_name, src_dock) in candidates:
                            if size_indices[src_name] > max_index:
                                return (src_name, src_dock)
                        return (None, None)

                    def pick_random_dst(src_name, src_dock):
                        for (dst_name, dst_dock) in candidates:
                            if are_rooms_compatible(src_name, src_dock, dst_name, dst_dock, self.configuration.room_rando):
                                return (dst_name, dst_dock)
                        return (None, None)

                    def remove_pair(shuffled_pair: set):
                        shuffled.remove(shuffled_pair)

                        shuffled_pair = sorted(list(shuffled_pair))
                        assert len(shuffled_pair) == 2
                        a = shuffled_pair[0]
                        b = shuffled_pair[1]

                        candidates.append(a)
                        candidates.append(b)

                        (a_name, a_dock) = a
                        (b_name, b_dock) = b
                        used_room_pairings.remove({a_name, b_name})

                        del world_data[world.name]["rooms"][a_name]["doors"][str(a_dock)]["destination"]
                        del world_data[world.name]["rooms"][b_name]["doors"][str(b_dock)]["destination"]

                    # Randomly pick room sources, starting with the largest room first, then randomly pick a compatible destination
                    max_index = 1.01
                    while len(candidates) != 0:
                        assert len(candidates) % 2 == 0

                        if max_index < -0.00001:
                            raise Exception("Failed to find pairings for %s" % str(candidates))

                        (src_name, src_dock) = next_candidate(max_index)

                        if src_name is None:
                            # lower the room size criteria and try again
                            max_index -= 0.01
                            continue

                        (dst_name, dst_dock) = pick_random_dst(src_name, src_dock)
                        if dst_name is None:
                            # This room have no valid destinations in the pool, randomly unpair two rooms and try again
                            remove_pair(self.rng.choice(shuffled))
                            continue

                        assert {(src_name, src_dock), (dst_name, dst_dock)} not in shuffled

                        candidates.remove((src_name, src_dock))
                        candidates.remove((dst_name, dst_dock))
                        shuffled.append({(src_name, src_dock), (dst_name, dst_dock)})
                        used_room_pairings.append({src_name, dst_name})

                        world_data[world.name]["rooms"][src_name]["doors"][str(src_dock)] = {
                            "destination": {
                                "roomName": dst_name,
                                "dockNum": dst_dock,
                            }
                        }
                        world_data[world.name]["rooms"][dst_name]["doors"][str(dst_dock)] = {
                                "destination": {
                                    "roomName": src_name,
                                    "dockNum": src_dock,
                                }
                            }

                        # print("%s:%d <--> %s:%d" % (src_name, src_dock, dst_name, dst_dock))

                        # If we just finished placing all rooms, check if there are unconnected components
                        # and if so, re-roll some rooms
                        if len(candidates) == 0:
                            import networkx

                            # Model as networkx graph object
                            room_connections = list()
                            for room_name in world_data[world.name]["rooms"].keys():
                                room = world_data[world.name]["rooms"][room_name]
                                if "doors" not in room.keys():
                                    continue
                                for dock_num in room["doors"]:
                                    if "destination" not in room["doors"][dock_num].keys():
                                        continue

                                    if (room_name, int(dock_num)) in disabled_doors:
                                        continue

                                    dst_room_name = room["doors"][dock_num]["destination"]["roomName"]

                                    assert {room_name, dst_room_name} in used_room_pairings
                                    
                                    room_connections.append((room_name, dst_room_name))

                            # Handle unrandomized connections
                            for (src_name, src_dock) in is_nonstandard:
                                if (src_name, src_dock) in disabled_doors:
                                    continue
                                if is_nonstandard[(src_name, src_dock)]:
                                    (dst_name, dst_dock) = default_connections[(src_name, src_dock)]
                                    room_connections.append((src_name, dst_name))

                            # model this world's connections as a graph
                            graph = networkx.DiGraph()
                            graph.add_edges_from(room_connections)

                            if not networkx.is_strongly_connected(graph):
                                # Split graph into strongly connected components
                                strongly_connected_components = sorted(networkx.strongly_connected_components(graph), key=len, reverse=True)
                                assert len(strongly_connected_components) > 1

                                def component_number(name):
                                    i = 0
                                    for component in strongly_connected_components:
                                        if name in list(component):
                                            return i
                                        i += 1    

                                # randomply pick two room pairs which are not members of the same strongly connected component and
                                # put back into pool for re-randomization (cross fingers that they connect the two strong components)
                                assert len(shuffled) > 2

                                # pick one randomly
                                self.rng.shuffle(shuffled)
                                a = shuffled[-1]
                                a = sorted(list(a))
                                (src_name_a, src_dock_a) = a[0]
                                (dst_name_a, dst_dock_a) = a[1]
                                a_component_num = component_number(src_name_a)

                                # pick a second which is not part of the same component
                                (src_name_b, src_dock_b, dst_name_b, dst_dock_b) = (None, None, None, None)
                                for b in shuffled:
                                    b = sorted(list(b))
                                    (src_name, src_dock) = b[0]
                                    (dst_name, dst_dock) = b[1]
                                    if component_number(src_name) == a_component_num:
                                        continue
                                    (src_name_b, src_dock_b, dst_name_b, dst_dock_b) = (src_name, src_dock, dst_name, dst_dock)
                                    break

                                # If we could not find two rooms that were part of two different components, still remove a random room pairing
                                # (this can happen if rooms exempt from randomization are causing fractured connectivity)
                                if src_name_b is None:
                                    b = sorted(list(shuffled[0]))
                                    (src_name_b, src_dock_b) = b[0]
                                    (dst_name_b, dst_dock_b) = b[1]

                                # put back into pool
                                remove_pair({(src_name_a, src_dock_a), (dst_name_a, dst_dock_a)})
                                remove_pair({(src_name_b, src_dock_b), (dst_name_b, dst_dock_b)})

                                # do something different this time
                                self.rng.shuffle(candidates)

        if self.configuration.hints.phazon_suit != PhazonSuitHintMode.DISABLED:
            phazon_suit_resource_info = self.game.resource_database.get_item_by_name("Phazon Suit")

            hint_texts: dict[ItemResourceInfo, str] = guaranteed_item_hint.create_guaranteed_hints_for_resources(
                self.description.all_patches,
                self.players_config,
                namer,
                    self.configuration.hints.phazon_suit == PhazonSuitHintMode.HIDE_AREA,
                [phazon_suit_resource_info],
                True,
            )

            phazon_hint_text = hint_texts[phazon_suit_resource_info]

            world_data["Impact Crater"]["rooms"]["Crater Entry Point"]["extraScans"] = [
                {
                    "position": [
                        -19.4009,
                        41.001,
                        2.805
                    ],
                    "combatVisible": True,
                    "text": phazon_hint_text,
                    "rotation": 45.0,
                    "isRed": True,
                    "logbookTitle": "Phazon Suit",
                    "logbookCategory": 5 # Artifacts
                }
            ]

        starting_memo = None
        extra_starting = item_names.additional_starting_items(self.configuration, db.resource_database,
                                                              self.patches.starting_items)
        if extra_starting:
            starting_memo = ", ".join(extra_starting)

        if self.cosmetic_patches.open_map and self.configuration.elevators.is_vanilla:
            map_default_state = "visible"
        else:
            map_default_state = "default"

        credits_string = credits_spoiler.prime_trilogy_credits(
            self.configuration.major_items_configuration,
            self.description.all_patches,
            self.players_config,
            namer,
            "&push;&font=C29C51F1;&main-color=#89D6FF;Major Item Locations&pop;",
            "&push;&font=C29C51F1;&main-color=#33ffd6;{}&pop;",
        )

        artifacts = [
            db.resource_database.get_item(index)
            for index in prime_items.ARTIFACT_ITEMS
        ]
        hint_config = self.configuration.hints
        if hint_config.artifacts == ArtifactHintMode.DISABLED:
            resulting_hints = {art: "{} is lost somewhere on Tallon IV.".format(art.long_name) for art in artifacts}
        else:
            resulting_hints = guaranteed_item_hint.create_guaranteed_hints_for_resources(
                self.description.all_patches,
                self.players_config, namer,
                hint_config.artifacts == ArtifactHintMode.HIDE_AREA,
                [db.resource_database.get_item(index) for index in prime_items.ARTIFACT_ITEMS],
                True,
            )

        # Tweaks
        ctwk_config = {}
        if self.configuration.small_samus:
            ctwk_config["playerSize"] = 0.3
            ctwk_config["morphBallSize"] = 0.3
            ctwk_config["easyLavaEscape"] = True
        
        if self.configuration.large_samus:
            ctwk_config["playerSize"] = 1.75

        if self.cosmetic_patches.use_hud_color:
            ctwk_config["hudColor"] = [
                self.cosmetic_patches.hud_color[0] / 255,
                self.cosmetic_patches.hud_color[1] / 255,
                self.cosmetic_patches.hud_color[2] / 255
            ]

        SUIT_ATTRIBUTES = ["powerDeg", "variaDeg", "gravityDeg", "phazonDeg"]
        suit_colors = {}
        for attribute, hue_rotation in zip(SUIT_ATTRIBUTES, self.cosmetic_patches.suit_color_rotations):
            if hue_rotation != 0:
                suit_colors[attribute] = hue_rotation

        starting_room = _name_for_location(db.world_list, self.patches.starting_location)

        starting_items = {
            name: _starting_items_value_for(db.resource_database, self.patches.starting_items, index)
            for name, index in _STARTING_ITEM_NAME_TO_INDEX.items()
        }

        if self.configuration.deterministic_idrone:
            idrone_config = {
                "eyeWaitInitialRandomTime": 0.0,
                "eyeWaitRandomTime": 0.0,
                "eyeStayUpRandomTime": 0.0,
                "resetContraptionRandomTime": 0.0,
                # ~~~ Justification for Divide by 2 ~~~
                # These Timer RNG values are normally re-rolled inbetween each of the 4 phases,
                # turning the zoid fight duration probability into a bell curve. With /2 we manipulate
                # the (now linear) probability characteristic to more often generate "average zoid fights"
                # while erring on the side of faster.
                "eyeWaitInitialMinimumTime": 8.0 + self.rng.random() * 5.0 / 2.0,
                "eyeWaitMinimumTime": 15.0 + self.rng.random() * 10.0 / 2.0,
                "eyeStayUpMinimumTime": 8.0 + self.rng.random() * 3.0 / 2.0,
                "resetContraptionMinimumTime": 3.0 + self.rng.random() * 3.0 / 2.0,
            }
        else:
            idrone_config = None
        
        if self.configuration.deterministic_maze:
            maze_seeds = [self.rng.choice(VANILLA_MAZE_SEEDS)]
        else:
            maze_seeds = None

        seed = self.description.get_seed_for_player(self.players_config.player_index)

        boss_sizes = None
        if self.configuration.random_boss_sizes:
            def get_random_size(minimum, maximum):
                if self.rng.choice([True, False]):
                    temp = [self.rng.uniform(minimum, 1.0), self.rng.uniform(minimum, 1.0)]
                    return min(temp)
                else:
                    temp = [self.rng.uniform(1.0, maximum), self.rng.uniform(1.0, maximum)]
                    return max(temp)

            boss_sizes = {
                "parasiteQueen": get_random_size(0.1, 3.0),
                "incineratorDrone": get_random_size(0.2, 3.0),
                "adultSheegoth": get_random_size(0.1, 1.5),
                "thardus": get_random_size(0.05, 2.0),
                "elitePirate1": get_random_size(0.05, 2.3),
                "elitePirate2": get_random_size(0.05, 1.3),
                "elitePirate3": get_random_size(0.05, 2.0),
                "phazonElite": get_random_size(0.1, 2.0),
                "omegaPirate": get_random_size(0.05, 2.0),
                "Ridley": get_random_size(0.2, 1.5),
                "exo": get_random_size(0.15, 2.0),
                "essence": get_random_size(0.05, 2.25),
                "flaahgra": get_random_size(0.15, 3.3),
                "platedBeetle": get_random_size(0.05, 6.0),
                "cloakedDrone": get_random_size(0.05, 6.0), # only scales width (lmao)
            }

        return {
            "seed": seed,
            "preferences": {
                "defaultGameOptions": self.get_default_game_options(),
                "qolGameBreaking": self.configuration.qol_game_breaking,
                "qolCosmetic": self.cosmetic_patches.qol_cosmetic,
                "qolPickupScans": self.configuration.qol_pickup_scans,
                "qolCutscenes": self.configuration.qol_cutscenes.value,
                "mapDefaultState": map_default_state,
                "artifactHintBehavior": None,
                "automaticCrashScreen": True,
                "trilogyDiscPath": None,
                "quickplay": False,
                "quiet": False,
                "suitColors": suit_colors,
            },
            "gameConfig": {
                "bossSizes": boss_sizes,
                "noDoors": self.configuration.no_doors,
                "shufflePickupPosition": self.configuration.shuffle_item_pos,
                "shufflePickupPosAllRooms": False, # functionality is handled in randovania as of v4.3
                "startingRoom": starting_room,
                "startingMemo": starting_memo,
                "warpToStart": self.configuration.warp_to_start,
                "springBall": self.configuration.spring_ball,
                "incineratorDroneConfig": idrone_config,
                "mazeSeeds": maze_seeds,
                "nonvariaHeatDamage": self.configuration.heat_protection_only_varia,
                "staggeredSuitDamage": self.configuration.progressive_damage_reduction,
                "heatDamagePerSec": self.configuration.heat_damage,
                "autoEnabledElevators": self.patches.starting_items.get(scan_visor, 0) == 0,
                "multiworldDolPatches": True,

                "disableItemLoss": True,  # Item Loss in Frigate
                "startingItems": starting_items,

                "etankCapacity": self.configuration.energy_per_tank,
                "itemMaxCapacity": {
                    "Energy Tank": db.resource_database.get_item("EnergyTank").max_capacity,
                    "Power Bomb": db.resource_database.get_item("PowerBomb").max_capacity,
                    "Missile": db.resource_database.get_item("Missile").max_capacity,
                    "Unknown Item 1": db.resource_database.multiworld_magic_item.max_capacity,
                },

                "mainPlazaDoor": self.configuration.main_plaza_door,
                "backwardsFrigate": self.configuration.backwards_frigate,
                "backwardsLabs": self.configuration.backwards_labs,
                "backwardsUpperMines": self.configuration.backwards_upper_mines,
                "backwardsLowerMines": self.configuration.backwards_lower_mines,
                "phazonEliteWithoutDynamo": self.configuration.phazon_elite_without_dynamo,

                "gameBanner": {
                    "gameName": "Metroid Prime: Randomizer",
                    "gameNameFull": "Metroid Prime: Randomizer - {}".format(self.description.shareable_hash),
                    "description": "Seed Hash: {}".format(self.description.shareable_word_hash),
                },
                "mainMenuMessage": "Randovania v{}\n{}".format(
                    randovania.VERSION,
                    self.description.shareable_word_hash
                ),

                "creditsString": credits_string,
                "artifactHints": {
                    artifact.long_name: text
                    for artifact, text in resulting_hints.items()
                },
                "artifactTempleLayerOverrides": {
                    artifact.long_name: self.patches.starting_items.get(artifact, 0) == 0
                    for artifact in artifacts
                },
            },
            "tweaks": ctwk_config,
            "levelData": world_data,
            "hasSpoiler": self.description.has_spoiler,
            "roomRandoMode": self.configuration.room_rando.value,

            # TODO
            # "externAssetsDir": path_to_converted_assets,
        }
def test_create_pickup_list(model_style: PickupModelStyle, empty_patches, generic_item_category,
                            blank_resource_db):
    # Setup
    has_scan_text = model_style in {PickupModelStyle.ALL_VISIBLE, PickupModelStyle.HIDE_MODEL}
    rng = Random(5000)

    model_0 = MagicMock(spec=PickupModel)
    model_1 = MagicMock(spec=PickupModel)
    model_2 = MagicMock(spec=PickupModel)
    useless_model = PickupModel(
        game=RandovaniaGame.METROID_PRIME_ECHOES,
        name="EnergyTransferModule",
    )

    useless_resource =  ItemResourceInfo(0, "Useless", "Useless", 10)
    resource_a = ItemResourceInfo(1, "A", "A", 10)
    resource_b = ItemResourceInfo(2, "B", "B", 10)
    pickup_a = PickupEntry("P-A", model_1, generic_item_category, generic_item_category,
                           progression=((resource_a, 1),),
                           )
    pickup_b = PickupEntry("P-B", model_2, generic_item_category, generic_item_category,
                           progression=((resource_b, 1),
                                        (resource_a, 5)), )
    pickup_c = PickupEntry("P-C", model_2, AMMO_ITEM_CATEGORY, generic_item_category,
                           progression=tuple(),
                           extra_resources=((resource_b, 2), (resource_a, 1)),
                           unlocks_resource=True,
                           resource_lock=ResourceLock(resource_a, resource_a, useless_resource))

    useless_pickup = PickupEntry("P-Useless", model_0, USELESS_ITEM_CATEGORY, USELESS_ITEM_CATEGORY,
                                 progression=((useless_resource, 1),))
    patches = empty_patches.assign_new_pickups([
        (PickupIndex(0), PickupTarget(pickup_a, 0)),
        (PickupIndex(2), PickupTarget(pickup_b, 0)),
        (PickupIndex(3), PickupTarget(pickup_a, 0)),
        (PickupIndex(4), PickupTarget(pickup_c, 0)),
    ])
    creator = pickup_exporter.PickupExporterSolo(pickup_exporter.GenericAcquiredMemo())

    world_list = MagicMock()
    world_list.iterate_nodes.return_value = [
        PickupNode(NodeIdentifier.create("World", "Area", f"Name {i}"),
                   i, False, None, "", ("default",), {}, PickupIndex(i), False)
        for i in range(5)
    ]

    # Run
    result = pickup_exporter.export_all_indices(
        patches,
        PickupTarget(useless_pickup, 0),
        world_list,
        rng,
        model_style,
        PickupModelDataSource.ETM,
        creator,
        pickup_creator.create_visual_etm(),
    )

    # Assert
    assert len(result) == 5
    assert result[0] == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(0),
        scan_text="P-A" if has_scan_text else "Unknown item",
        hud_text=["A acquired!"] if model_style != PickupModelStyle.HIDE_ALL else ['Unknown item acquired!'],
        conditional_resources=[ConditionalResources("A", None, ((resource_a, 1),))],
        conversion=[],
        model=model_1 if model_style == PickupModelStyle.ALL_VISIBLE else useless_model,
        other_player=False,
        original_pickup=pickup_a,
    )
    assert result[1] == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(1),
        scan_text="P-Useless" if has_scan_text else "Unknown item",
        hud_text=["Useless acquired!"] if model_style != PickupModelStyle.HIDE_ALL else ['Unknown item acquired!'],
        conditional_resources=[ConditionalResources("Useless", None, ((useless_resource, 1),))],
        conversion=[],
        model=model_0 if model_style == PickupModelStyle.ALL_VISIBLE else useless_model,
        other_player=False,
        original_pickup=useless_pickup,
    )
    assert result[2] == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(2),
        scan_text="P-B. Provides the following in order: B, A" if has_scan_text else "Unknown item",
        hud_text=["B acquired!", "A acquired!"] if model_style != PickupModelStyle.HIDE_ALL else [
            'Unknown item acquired!', 'Unknown item acquired!'],
        conditional_resources=[
            ConditionalResources("B", None, ((resource_b, 1),)),
            ConditionalResources("A", resource_b, ((resource_a, 5),)),
        ],
        conversion=[],
        model=model_2 if model_style == PickupModelStyle.ALL_VISIBLE else useless_model,
        other_player=False,
        original_pickup=pickup_b,
    )
    assert result[3] == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(3),
        scan_text="P-A" if has_scan_text else "Unknown item",
        hud_text=["A acquired!"] if model_style != PickupModelStyle.HIDE_ALL else ['Unknown item acquired!'],
        conditional_resources=[ConditionalResources("A", None, ((resource_a, 1),))],
        conversion=[],
        model=model_1 if model_style == PickupModelStyle.ALL_VISIBLE else useless_model,
        other_player=False,
        original_pickup=pickup_a,
    )
    assert result[4] == pickup_exporter.ExportedPickupDetails(
        index=PickupIndex(4),
        scan_text="P-C. Provides 2 B and 1 A" if has_scan_text else "Unknown item",
        hud_text=["P-C acquired!"] if model_style != PickupModelStyle.HIDE_ALL else ['Unknown item acquired!'],
        conditional_resources=[ConditionalResources("P-C", None, (
            (resource_b, 2), (resource_a, 1),
        ))],
        conversion=[ResourceConversion(source=useless_resource, target=resource_a)],
        model=model_2 if model_style == PickupModelStyle.ALL_VISIBLE else useless_model,
        other_player=False,
        original_pickup=pickup_c,
    )