コード例 #1
0
    def __init__(self, window_manager: Optional[WindowManager],
                 editor: PresetEditor):
        super().__init__()
        self.setupUi(self)
        common_qt_lib.set_default_window_icon(self)

        self._editor = editor
        self._window_manager = window_manager
        self._main_rules = MainRulesWindow(editor)
        self._game_patches = GamePatchesWindow(editor)

        self.game_description = default_database.default_prime2_game_description(
        )
        self.world_list = self.game_description.world_list
        self.resource_database = self.game_description.resource_database

        # Update with Options
        self.logic_tab_widget.addTab(self._main_rules.centralWidget,
                                     "Item Pool")
        self.patches_tab_widget.addTab(self._game_patches.centralWidget,
                                       "Other")

        self.name_edit.textEdited.connect(self._edit_name)
        self.setup_trick_level_elements()
        self.setup_damage_elements()
        self.setup_elevator_elements()
        self.setup_sky_temple_elements()
        self.setup_starting_area_elements()
        self.setup_location_pool_elements()
        self.setup_translators_elements()
        self.setup_hint_elements()
        self.setup_beam_configuration_elements()

        # Alignment
        self.trick_level_layout.setAlignment(QtCore.Qt.AlignTop)
        self.elevator_layout.setAlignment(QtCore.Qt.AlignTop)
        self.goal_layout.setAlignment(QtCore.Qt.AlignTop)
        self.starting_area_layout.setAlignment(QtCore.Qt.AlignTop)
        self.translators_layout.setAlignment(QtCore.Qt.AlignTop)
        self.hint_layout.setAlignment(QtCore.Qt.AlignTop)

        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.reject)
コード例 #2
0
class LogicSettingsWindow(QDialog, Ui_LogicSettingsWindow):
    _combo_for_gate: Dict[TranslatorGate, QComboBox]
    _checkbox_for_trick: Dict[SimpleResourceInfo, QtWidgets.QCheckBox]
    _location_pool_for_node: Dict[PickupNode, QtWidgets.QCheckBox]
    _starting_location_for_area: Dict[int, QtWidgets.QCheckBox]
    _slider_for_trick: Dict[SimpleResourceInfo, QtWidgets.QSlider]
    _editor: PresetEditor
    world_list: WorldList
    _during_batch_check_update: bool = False

    def __init__(self, window_manager: WindowManager, editor: PresetEditor):
        super().__init__()
        self.setupUi(self)
        common_qt_lib.set_default_window_icon(self)

        self._editor = editor
        self._window_manager = window_manager
        self._main_rules = MainRulesWindow(editor)
        self._game_patches = GamePatchesWindow(editor)

        self.game_description = default_database.default_prime2_game_description(
        )
        self.world_list = self.game_description.world_list
        self.resource_database = self.game_description.resource_database

        # Update with Options
        self.logic_tab_widget.addTab(self._main_rules.centralWidget,
                                     "Item Pool")
        self.patches_tab_widget.addTab(self._game_patches.centralWidget,
                                       "Other")

        self.name_edit.textEdited.connect(self._edit_name)
        self.setup_trick_level_elements()
        self.setup_damage_elements()
        self.setup_elevator_elements()
        self.setup_sky_temple_elements()
        self.setup_starting_area_elements()
        self.setup_location_pool_elements()
        self.setup_translators_elements()
        self.setup_hint_elements()
        self.setup_beam_configuration_elements()

        # Alignment
        self.trick_level_layout.setAlignment(QtCore.Qt.AlignTop)
        self.elevator_layout.setAlignment(QtCore.Qt.AlignTop)
        self.goal_layout.setAlignment(QtCore.Qt.AlignTop)
        self.starting_area_layout.setAlignment(QtCore.Qt.AlignTop)
        self.translators_layout.setAlignment(QtCore.Qt.AlignTop)
        self.hint_layout.setAlignment(QtCore.Qt.AlignTop)

        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.reject)

    # Options
    def on_preset_changed(self, preset: Preset):
        self._main_rules.on_preset_changed(preset)
        self._game_patches.on_preset_changed(preset)

        # Variables
        layout_config = preset.layout_configuration
        patcher_config = preset.patcher_configuration

        # Title
        common_qt_lib.set_edit_if_different(self.name_edit, preset.name)

        # Trick Level
        trick_level_configuration = preset.layout_configuration.trick_level_configuration
        trick_level = trick_level_configuration.global_level

        set_combo_with_value(self.logic_combo_box, trick_level)
        self.logic_level_label.setText(
            _get_trick_level_description(trick_level))

        for (trick, checkbox), slider in zip(self._checkbox_for_trick.items(),
                                             self._slider_for_trick.values()):
            assert self._slider_for_trick[trick] is slider

            has_specific_level = trick_level_configuration.has_specific_level_for_trick(
                trick)

            checkbox.setEnabled(
                trick_level != LayoutTrickLevel.MINIMAL_RESTRICTIONS)
            slider.setEnabled(has_specific_level)
            slider.setValue(
                trick_level_configuration.level_for_trick(trick).as_number)
            checkbox.setChecked(has_specific_level)

        # Damage
        set_combo_with_value(self.damage_strictness_combo,
                             layout_config.damage_strictness)
        self.energy_tank_capacity_spin_box.setValue(
            layout_config.energy_per_tank)
        self.varia_suit_spin_box.setValue(patcher_config.varia_suit_damage)
        self.dark_suit_spin_box.setValue(patcher_config.dark_suit_damage)

        # Elevator
        set_combo_with_value(self.elevators_combo, layout_config.elevators)

        # Sky Temple Keys
        keys = layout_config.sky_temple_keys
        if isinstance(keys.value, int):
            self.skytemple_slider.setValue(keys.value)
            data = int
        else:
            data = keys
        set_combo_with_value(self.skytemple_combo, data)

        # Starting Area
        starting_locations = layout_config.starting_location.locations

        self._during_batch_check_update = True
        for world in self.game_description.world_list.worlds:
            for area in world.areas:
                is_checked = AreaLocation(
                    world.world_asset_id,
                    area.area_asset_id) in starting_locations
                self._starting_location_for_area[
                    area.area_asset_id].setChecked(is_checked)
        self._during_batch_check_update = False

        # Location Pool
        available_locations = layout_config.available_locations
        set_combo_with_value(self.randomization_mode_combo,
                             available_locations.randomization_mode)

        self._during_batch_check_update = True
        for node, check in self._location_pool_for_node.items():
            check.setChecked(
                node.pickup_index not in available_locations.excluded_indices)
            check.setEnabled(available_locations.randomization_mode
                             == RandomizationMode.FULL or node.major_location)
        self._during_batch_check_update = False

        # Translator Gates
        translator_configuration = preset.layout_configuration.translator_configuration
        for gate, combo in self._combo_for_gate.items():
            set_combo_with_value(
                combo, translator_configuration.translator_requirement[gate])

        # Hints
        set_combo_with_value(self.hint_sky_temple_key_combo,
                             preset.layout_configuration.hints.sky_temple_keys)

        # Beam Configuration
        self.on_preset_changed_beam_configuration(preset)

    def _edit_name(self, value: str):
        with self._editor as editor:
            editor.name = value

    # Trick Level

    def _create_difficulty_details_row(self):
        row = 1

        trick_label = QtWidgets.QLabel(self.trick_level_scroll_contents)
        trick_label.setWordWrap(True)
        trick_label.setFixedWidth(80)
        trick_label.setText("Difficulty Details")

        self.trick_difficulties_layout.addWidget(trick_label, row, 1, 1, 1)

        slider_layout = QtWidgets.QGridLayout()
        slider_layout.setHorizontalSpacing(0)
        for i in range(12):
            slider_layout.setColumnStretch(i, 1)

        for i, trick_level in enumerate(LayoutTrickLevel):
            if trick_level not in {
                    LayoutTrickLevel.NO_TRICKS,
                    LayoutTrickLevel.MINIMAL_RESTRICTIONS
            }:
                tool_button = QtWidgets.QToolButton(
                    self.trick_level_scroll_contents)
                tool_button.setText(trick_level.long_name)
                tool_button.clicked.connect(
                    functools.partial(self._open_difficulty_details_popup,
                                      trick_level))

                slider_layout.addWidget(tool_button, 1, 2 * i, 1, 2)

        self.trick_difficulties_layout.addLayout(slider_layout, row, 2, 1, 1)

    def setup_trick_level_elements(self):
        # logic_combo_box
        for i, trick_level in enumerate(LayoutTrickLevel):
            self.logic_combo_box.setItemData(i, trick_level)

        self.logic_combo_box.currentIndexChanged.connect(
            self._on_trick_level_changed)

        self.trick_difficulties_layout = QtWidgets.QGridLayout()
        self._checkbox_for_trick = {}
        self._slider_for_trick = {}

        configurable_tricks = TrickLevelConfiguration.all_possible_tricks()
        tricks_in_use = used_tricks(self.world_list)

        size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed,
                                            QtWidgets.QSizePolicy.Preferred)

        self._create_difficulty_details_row()

        row = 2
        for trick in sorted(self.resource_database.trick,
                            key=lambda _trick: _trick.long_name):
            if trick.index not in configurable_tricks or trick not in tricks_in_use:
                continue

            if row > 1:
                self.trick_difficulties_layout.addItem(
                    QtWidgets.QSpacerItem(20, 40,
                                          QtWidgets.QSizePolicy.Minimum,
                                          QtWidgets.QSizePolicy.Expanding))

            trick_configurable = QtWidgets.QCheckBox(
                self.trick_level_scroll_contents)
            trick_configurable.setFixedWidth(16)
            trick_configurable.stateChanged.connect(
                functools.partial(self._on_check_trick_configurable, trick))
            self._checkbox_for_trick[trick] = trick_configurable
            self.trick_difficulties_layout.addWidget(trick_configurable, row,
                                                     0, 1, 1)

            trick_label = QtWidgets.QLabel(self.trick_level_scroll_contents)
            trick_label.setSizePolicy(size_policy)
            trick_label.setWordWrap(True)
            trick_label.setFixedWidth(80)
            trick_label.setText(trick.long_name)

            self.trick_difficulties_layout.addWidget(trick_label, row, 1, 1, 1)

            slider_layout = QtWidgets.QGridLayout()
            slider_layout.setHorizontalSpacing(0)
            for i in range(12):
                slider_layout.setColumnStretch(i, 1)

            horizontal_slider = QtWidgets.QSlider(
                self.trick_level_scroll_contents)
            horizontal_slider.setMaximum(5)
            horizontal_slider.setPageStep(2)
            horizontal_slider.setOrientation(QtCore.Qt.Horizontal)
            horizontal_slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
            horizontal_slider.setEnabled(False)
            horizontal_slider.valueChanged.connect(
                functools.partial(self._on_slide_trick_slider, trick))
            self._slider_for_trick[trick] = horizontal_slider
            slider_layout.addWidget(horizontal_slider, 0, 1, 1, 10)

            used_difficulties = difficulties_for_trick(self.world_list, trick)
            for i, trick_level in enumerate(LayoutTrickLevel):
                if trick_level == LayoutTrickLevel.NO_TRICKS or trick_level in used_difficulties:
                    difficulty_label = QtWidgets.QLabel(
                        self.trick_level_scroll_contents)
                    difficulty_label.setAlignment(QtCore.Qt.AlignHCenter)
                    difficulty_label.setText(trick_level.long_name)

                    slider_layout.addWidget(difficulty_label, 1, 2 * i, 1, 2)

            self.trick_difficulties_layout.addLayout(slider_layout, row, 2, 1,
                                                     1)

            tool_button = QtWidgets.QToolButton(
                self.trick_level_scroll_contents)
            tool_button.setText("?")
            tool_button.clicked.connect(
                functools.partial(self._open_trick_details_popup, trick))
            self.trick_difficulties_layout.addWidget(tool_button, row, 3, 1, 1)

            row += 1

        self.trick_level_layout.addLayout(self.trick_difficulties_layout)

    def _on_check_trick_configurable(self, trick: SimpleResourceInfo,
                                     enabled: int):
        enabled = bool(enabled)

        with self._editor as options:
            if options.layout_configuration.trick_level_configuration.has_specific_level_for_trick(
                    trick) != enabled:
                options.set_layout_configuration_field(
                    "trick_level_configuration",
                    options.layout_configuration.trick_level_configuration.
                    set_level_for_trick(
                        trick,
                        self.logic_combo_box.currentData()
                        if enabled else None))

    def _on_slide_trick_slider(self, trick: SimpleResourceInfo, value: int):
        if self._slider_for_trick[trick].isEnabled():
            with self._editor as options:
                options.set_layout_configuration_field(
                    "trick_level_configuration",
                    options.layout_configuration.trick_level_configuration.
                    set_level_for_trick(trick,
                                        LayoutTrickLevel.from_number(value)))

    def _on_trick_level_changed(self):
        trick_level = self.logic_combo_box.currentData()
        with self._editor as options:
            options.set_layout_configuration_field(
                "trick_level_configuration",
                dataclasses.replace(
                    options.layout_configuration.trick_level_configuration,
                    global_level=trick_level))

    def _exec_trick_details(self, popup: TrickDetailsPopup):
        self._trick_details_popup = popup
        self._trick_details_popup.setWindowModality(Qt.WindowModal)
        self._trick_details_popup.open()

    def _open_trick_details_popup(self, trick: SimpleResourceInfo):
        self._exec_trick_details(
            TrickDetailsPopup(
                self,
                self._window_manager,
                self.game_description,
                trick,
                self._editor.layout_configuration.trick_level_configuration.
                level_for_trick(trick),
            ))

    def _open_difficulty_details_popup(self, difficulty: LayoutTrickLevel):
        self._exec_trick_details(
            TrickDetailsPopup(
                self,
                self._window_manager,
                self.game_description,
                None,
                difficulty,
            ))

    # Damage strictness
    def setup_damage_elements(self):
        self.damage_strictness_combo.setItemData(0,
                                                 LayoutDamageStrictness.STRICT)
        self.damage_strictness_combo.setItemData(1,
                                                 LayoutDamageStrictness.MEDIUM)
        self.damage_strictness_combo.setItemData(
            2, LayoutDamageStrictness.LENIENT)

        self.damage_strictness_combo.options_field_name = "layout_configuration_damage_strictness"
        self.damage_strictness_combo.currentIndexChanged.connect(
            functools.partial(_update_options_by_value, self._editor,
                              self.damage_strictness_combo))

        def _persist_float(attribute_name: str):
            def persist(value: float):
                with self._editor as options:
                    options.set_patcher_configuration_field(
                        attribute_name, value)

            return persist

        self.energy_tank_capacity_spin_box.valueChanged.connect(
            self._persist_tank_capacity)
        self.varia_suit_spin_box.valueChanged.connect(
            _persist_float("varia_suit_damage"))
        self.dark_suit_spin_box.valueChanged.connect(
            _persist_float("dark_suit_damage"))

    def _persist_tank_capacity(self):
        with self._editor as editor:
            editor.set_layout_configuration_field(
                "energy_per_tank", self.energy_tank_capacity_spin_box.value())

    # Elevator
    def setup_elevator_elements(self):
        self.elevators_combo.setItemData(0, LayoutElevators.VANILLA)
        self.elevators_combo.setItemData(1, LayoutElevators.TWO_WAY_RANDOMIZED)
        self.elevators_combo.setItemData(2, LayoutElevators.TWO_WAY_UNCHECKED)
        self.elevators_combo.setItemData(3, LayoutElevators.ONE_WAY_ELEVATOR)
        self.elevators_combo.setItemData(4, LayoutElevators.ONE_WAY_ANYTHING)

        self.elevators_combo.options_field_name = "layout_configuration_elevators"
        self.elevators_combo.currentIndexChanged.connect(
            functools.partial(_update_options_by_value, self._editor,
                              self.elevators_combo))

    # Sky Temple Key
    def setup_sky_temple_elements(self):
        self.skytemple_combo.setItemData(0, LayoutSkyTempleKeyMode.ALL_BOSSES)
        self.skytemple_combo.setItemData(1,
                                         LayoutSkyTempleKeyMode.ALL_GUARDIANS)
        self.skytemple_combo.setItemData(2, int)

        self.skytemple_combo.options_field_name = "layout_configuration_sky_temple_keys"
        self.skytemple_combo.currentIndexChanged.connect(
            self._on_sky_temple_key_combo_changed)
        self.skytemple_slider.valueChanged.connect(
            self._on_sky_temple_key_combo_slider_changed)

    def _on_sky_temple_key_combo_changed(self):
        combo_enum = self.skytemple_combo.currentData()
        with self._editor:
            if combo_enum is int:
                self.skytemple_slider.setEnabled(True)
                self._editor.layout_configuration_sky_temple_keys = LayoutSkyTempleKeyMode(
                    self.skytemple_slider.value())
            else:
                self.skytemple_slider.setEnabled(False)
                self._editor.layout_configuration_sky_temple_keys = combo_enum

    def _on_sky_temple_key_combo_slider_changed(self):
        self.skytemple_slider_label.setText(str(self.skytemple_slider.value()))
        self._on_sky_temple_key_combo_changed()

    # Starting Area
    def setup_starting_area_elements(self):
        game_description = default_prime2_game_description()
        world_to_group = {}
        self._starting_location_for_area = {}

        for row, world in enumerate(game_description.world_list.worlds):
            for column, is_dark_world in enumerate([False, True]):
                group_box = QGroupBox(self.starting_locations_contents)
                group_box.setTitle(world.correct_name(is_dark_world))
                vertical_layout = QVBoxLayout(group_box)
                vertical_layout.setContentsMargins(8, 4, 8, 4)
                vertical_layout.setSpacing(2)
                vertical_layout.setAlignment(QtCore.Qt.AlignTop)
                group_box.vertical_layout = vertical_layout

                world_to_group[world.correct_name(is_dark_world)] = group_box
                self.starting_locations_layout.addWidget(
                    group_box, row, column)

        for world in game_description.world_list.worlds:
            for area in sorted(world.areas, key=lambda a: a.name):
                group_box = world_to_group[world.correct_name(
                    area.in_dark_aether)]
                check = QtWidgets.QCheckBox(group_box)
                check.setText(area.name)
                check.area_location = AreaLocation(world.world_asset_id,
                                                   area.area_asset_id)
                check.stateChanged.connect(
                    functools.partial(self._on_check_starting_area, check))
                group_box.vertical_layout.addWidget(check)
                self._starting_location_for_area[area.area_asset_id] = check

        self.starting_area_quick_fill_ship.clicked.connect(
            self._starting_location_on_select_ship)
        self.starting_area_quick_fill_save_station.clicked.connect(
            self._starting_location_on_select_save_station)

    def _on_check_starting_area(self, check, _):
        if self._during_batch_check_update:
            return
        with self._editor as editor:
            editor.set_layout_configuration_field(
                "starting_location",
                editor.layout_configuration.starting_location.
                ensure_has_location(check.area_location, check.isChecked()))

    def _starting_location_on_select_ship(self):
        with self._editor as editor:
            editor.set_layout_configuration_field(
                "starting_location",
                StartingLocation.with_elements(
                    [self.game_description.starting_location]))

    def _starting_location_on_select_save_station(self):
        world_list = self.game_description.world_list
        save_stations = [
            world_list.node_to_area_location(node)
            for node in world_list.all_nodes if node.name == "Save Station"
        ]

        with self._editor as editor:
            editor.set_layout_configuration_field(
                "starting_location",
                StartingLocation.with_elements(save_stations))

    # Location Pool
    def setup_location_pool_elements(self):
        self.randomization_mode_combo.setItemData(0, RandomizationMode.FULL)
        self.randomization_mode_combo.setItemData(
            1, RandomizationMode.MAJOR_MINOR_SPLIT)
        self.randomization_mode_combo.currentIndexChanged.connect(
            self._on_update_randomization_mode)

        game_description = default_prime2_game_description()
        world_to_group = {}
        self._location_pool_for_node = {}

        for world in game_description.world_list.worlds:
            for is_dark_world in [False, True]:
                group_box = QGroupBox(self.excluded_locations_area_contents)
                group_box.setTitle(world.correct_name(is_dark_world))
                vertical_layout = QVBoxLayout(group_box)
                vertical_layout.setContentsMargins(8, 4, 8, 4)
                vertical_layout.setSpacing(2)
                group_box.vertical_layout = vertical_layout

                world_to_group[world.correct_name(is_dark_world)] = group_box
                self.excluded_locations_area_layout.addWidget(group_box)

        for world, area, node in game_description.world_list.all_worlds_areas_nodes:
            if not isinstance(node, PickupNode):
                continue

            group_box = world_to_group[world.correct_name(area.in_dark_aether)]
            check = QtWidgets.QCheckBox(group_box)
            check.setText(game_description.world_list.node_name(node))
            check.node = node
            check.stateChanged.connect(
                functools.partial(self._on_check_location, check))
            group_box.vertical_layout.addWidget(check)
            self._location_pool_for_node[node] = check

    def _on_update_randomization_mode(self):
        with self._editor as editor:
            editor.available_locations = dataclasses.replace(
                editor.available_locations,
                randomization_mode=self.randomization_mode_combo.currentData())

    def _on_check_location(self, check: QtWidgets.QCheckBox, _):
        if self._during_batch_check_update:
            return
        with self._editor as editor:
            editor.available_locations = editor.available_locations.ensure_index(
                check.node.pickup_index, not check.isChecked())

    # Translator Gates
    def setup_translators_elements(self):
        randomizer_data = default_data.decode_randomizer_data()

        self.translator_randomize_all_button.clicked.connect(
            self._on_randomize_all_gates_pressed)
        self.translator_vanilla_actual_button.clicked.connect(
            self._on_vanilla_actual_gates_pressed)
        self.translator_vanilla_colors_button.clicked.connect(
            self._on_vanilla_colors_gates_pressed)

        self._combo_for_gate = {}

        for i, gate in enumerate(randomizer_data["TranslatorLocationData"]):
            label = QLabel(self.translators_scroll_contents)
            label.setText(gate["Name"])
            self.translators_layout.addWidget(label, 3 + i, 0, 1, 1)

            combo = QComboBox(self.translators_scroll_contents)
            combo.gate = TranslatorGate(gate["Index"])
            for item in LayoutTranslatorRequirement:
                combo.addItem(item.long_name, item)
            combo.currentIndexChanged.connect(
                functools.partial(self._on_gate_combo_box_changed, combo))

            self.translators_layout.addWidget(combo, 3 + i, 1, 1, 2)
            self._combo_for_gate[combo.gate] = combo

    def _on_randomize_all_gates_pressed(self):
        with self._editor as options:
            options.set_layout_configuration_field(
                "translator_configuration",
                options.layout_configuration.translator_configuration.
                with_full_random())

    def _on_vanilla_actual_gates_pressed(self):
        with self._editor as options:
            options.set_layout_configuration_field(
                "translator_configuration",
                options.layout_configuration.translator_configuration.
                with_vanilla_actual())

    def _on_vanilla_colors_gates_pressed(self):
        with self._editor as options:
            options.set_layout_configuration_field(
                "translator_configuration",
                options.layout_configuration.translator_configuration.
                with_vanilla_colors())

    def _on_gate_combo_box_changed(self, combo: QComboBox, new_index: int):
        with self._editor as options:
            options.set_layout_configuration_field(
                "translator_configuration",
                options.layout_configuration.translator_configuration.
                replace_requirement_for_gate(combo.gate, combo.currentData()))

    # Hints
    def setup_hint_elements(self):
        for i, stk_hint_mode in enumerate(SkyTempleKeyHintMode):
            self.hint_sky_temple_key_combo.setItemData(i, stk_hint_mode)

        self.hint_sky_temple_key_combo.currentIndexChanged.connect(
            self._on_stk_combo_changed)

    def _on_stk_combo_changed(self, new_index: int):
        with self._editor as options:
            options.set_layout_configuration_field(
                "hints",
                dataclasses.replace(options.layout_configuration.hints,
                                    sky_temple_keys=self.
                                    hint_sky_temple_key_combo.currentData()))

    # Beam Configuration
    def setup_beam_configuration_elements(self):
        def _add_header(text: str, col: int):
            label = QLabel(self.beam_configuration_group)
            label.setText(text)
            self.beam_configuration_layout.addWidget(label, 0, col)

        _add_header("Ammo A", 1)
        _add_header("Ammo B", 2)
        _add_header("Uncharged", 3)
        _add_header("Charged", 4)
        _add_header("Combo", 5)
        _add_header("Missiles for Combo", 6)

        self._beam_ammo_a = {}
        self._beam_ammo_b = {}
        self._beam_uncharged = {}
        self._beam_charged = {}
        self._beam_combo = {}
        self._beam_missile = {}

        def _create_ammo_combo():
            combo = QComboBox(self.beam_configuration_group)
            combo.addItem("None", -1)
            combo.addItem("Power Bomb", 43)
            combo.addItem("Missile", 44)
            combo.addItem("Dark Ammo", 45)
            combo.addItem("Light Ammo", 46)
            return combo

        row = 1
        for beam, beam_name in _BEAMS.items():
            label = QLabel(self.beam_configuration_group)
            label.setText(beam_name)
            self.beam_configuration_layout.addWidget(label, row, 0)

            ammo_a = _create_ammo_combo()
            ammo_a.currentIndexChanged.connect(
                functools.partial(self._on_ammo_type_combo_changed, beam,
                                  ammo_a, False))
            self._beam_ammo_a[beam] = ammo_a
            self.beam_configuration_layout.addWidget(ammo_a, row, 1)

            ammo_b = _create_ammo_combo()
            ammo_b.currentIndexChanged.connect(
                functools.partial(self._on_ammo_type_combo_changed, beam,
                                  ammo_b, True))
            self._beam_ammo_b[beam] = ammo_b
            self.beam_configuration_layout.addWidget(ammo_b, row, 2)

            spin_box = QSpinBox(self.beam_configuration_group)
            spin_box.setSuffix(" ammo")
            spin_box.setMaximum(250)
            spin_box.valueChanged.connect(
                functools.partial(self._on_ammo_cost_spin_changed, beam,
                                  "uncharged_cost"))
            self._beam_uncharged[beam] = spin_box
            self.beam_configuration_layout.addWidget(spin_box, row, 3)

            spin_box = QSpinBox(self.beam_configuration_group)
            spin_box.setSuffix(" ammo")
            spin_box.setMaximum(250)
            spin_box.valueChanged.connect(
                functools.partial(self._on_ammo_cost_spin_changed, beam,
                                  "charged_cost"))
            self._beam_charged[beam] = spin_box
            self.beam_configuration_layout.addWidget(spin_box, row, 4)

            spin_box = QSpinBox(self.beam_configuration_group)
            spin_box.setSuffix(" ammo")
            spin_box.setMaximum(250)
            spin_box.valueChanged.connect(
                functools.partial(self._on_ammo_cost_spin_changed, beam,
                                  "combo_ammo_cost"))
            self._beam_combo[beam] = spin_box
            self.beam_configuration_layout.addWidget(spin_box, row, 5)

            spin_box = QSpinBox(self.beam_configuration_group)
            spin_box.setSuffix(" missile")
            spin_box.setMaximum(250)
            spin_box.setMinimum(1)
            spin_box.valueChanged.connect(
                functools.partial(self._on_ammo_cost_spin_changed, beam,
                                  "combo_missile_cost"))
            self._beam_missile[beam] = spin_box
            self.beam_configuration_layout.addWidget(spin_box, row, 6)

            row += 1

    def _on_ammo_type_combo_changed(self, beam: str, combo: QComboBox,
                                    is_ammo_b: bool, _):
        with self._editor as editor:
            beam_configuration = editor.layout_configuration.beam_configuration
            old_config: BeamAmmoConfiguration = getattr(
                beam_configuration, beam)
            if is_ammo_b:
                new_config = dataclasses.replace(old_config,
                                                 ammo_b=combo.currentData())
            else:
                new_config = dataclasses.replace(old_config,
                                                 ammo_a=combo.currentData())

            editor.set_layout_configuration_field(
                "beam_configuration",
                dataclasses.replace(beam_configuration, **{beam: new_config}))

    def _on_ammo_cost_spin_changed(self, beam: str, field_name: str,
                                   value: int):
        with self._editor as editor:
            beam_configuration = editor.layout_configuration.beam_configuration
            new_config = dataclasses.replace(getattr(beam_configuration, beam),
                                             **{field_name: value})
            editor.set_layout_configuration_field(
                "beam_configuration",
                dataclasses.replace(beam_configuration, **{beam: new_config}))

    def on_preset_changed_beam_configuration(self, preset: Preset):
        beam_configuration = preset.layout_configuration.beam_configuration

        for beam in _BEAMS:
            config: BeamAmmoConfiguration = getattr(beam_configuration, beam)

            self._beam_ammo_a[beam].setCurrentIndex(
                self._beam_ammo_a[beam].findData(config.ammo_a))
            self._beam_ammo_b[beam].setCurrentIndex(
                self._beam_ammo_b[beam].findData(config.ammo_b))
            self._beam_ammo_b[beam].setEnabled(config.ammo_a != -1)
            self._beam_uncharged[beam].setValue(config.uncharged_cost)
            self._beam_charged[beam].setValue(config.charged_cost)
            self._beam_combo[beam].setValue(config.combo_ammo_cost)
            self._beam_missile[beam].setValue(config.combo_missile_cost)