def patch_requirements(self, static_resources: ResourceCollection, damage_multiplier: float, database: ResourceDatabase) -> Requirement: if static_resources.is_resource_set(self.resource): if self.satisfied(static_resources, 0, database): return Requirement.trivial() else: return Requirement.impossible() else: return self.multiply_amount(damage_multiplier)
def simplify(self, keep_comments: bool = False) -> Requirement: new_items = expand_items(self.items, RequirementAnd, Requirement.trivial(), keep_comments) if Requirement.impossible() in new_items and mergeable_array( self, keep_comments): return Requirement.impossible() if len(new_items) == 1 and mergeable_array(self, keep_comments): return new_items[0] return RequirementAnd(new_items, comment=self.comment)
def simplify(self, keep_comments: bool = False) -> Requirement: new_items = expand_items(self.items, RequirementOr, Requirement.impossible(), keep_comments) if Requirement.trivial() in new_items and mergeable_array( self, keep_comments): return Requirement.trivial() num_and_requirements = 0 common_requirements = None for item in new_items: if isinstance(item, RequirementAnd) and mergeable_array( item, keep_comments): num_and_requirements += 1 if common_requirements is None: common_requirements = item.items else: common_requirements = [ common for common in common_requirements if common in item.items ] # Only extract the common requirements if there's more than 1 requirement if num_and_requirements >= 2 and common_requirements: simplified_items = [] common_new_or = [] for item in new_items: if isinstance(item, RequirementAnd) and mergeable_array( item, keep_comments): assert set(common_requirements) <= set(item.items) simplified_condition = [ it for it in item.items if it not in common_requirements ] if simplified_condition: common_new_or.append( RequirementAnd(simplified_condition ) if len(simplified_condition) > 1 else simplified_condition[0]) else: simplified_items.append(item) common_requirements.append(RequirementOr(common_new_or)) simplified_items.append(RequirementAnd(common_requirements)) final_items = simplified_items else: final_items = new_items if len(final_items) == 1 and mergeable_array(self, keep_comments): return final_items[0] return RequirementOr(final_items, comment=self.comment)
def test_requirement_as_set_4(database): def _req(name: str): id_req = ResourceRequirement.simple(database.get_item(name)) return id_req req = RequirementOr([ Requirement.impossible(), _req("A"), Requirement.trivial(), ]) assert req.as_set(database) == RequirementSet([ RequirementList([]), ])
def test_edit_connections(game_editor): # Setup landing_site = game_editor.game.world_list.area_by_area_location( AreaIdentifier("Temple Grounds", "Landing Site")) source = landing_site.node_with_name("Save Station") target = landing_site.node_with_name("Door to Service Access") assert landing_site.connections[source][target] != Requirement.trivial() # Run game_editor.edit_connections(landing_site, source, target, Requirement.trivial()) # Assert assert landing_site.connections[source][target] == Requirement.trivial()
def update_connections(self): current_node = self.current_node current_connection_node = self.current_connection_node assert current_node != current_connection_node or current_node is None if self._connections_visualizer is not None: self._connections_visualizer.deleteLater() self._connections_visualizer = None if current_connection_node is None or current_node is None: assert len(self.current_area.nodes) <= 1 or not self.edit_mode return requirement = self.current_area.connections[current_node].get( self.current_connection_node, Requirement.impossible()) if self._collection_for_filtering is not None: requirement = requirement.patch_requirements( self._collection_for_filtering, 1.0, self.game_description.resource_database, ) self._connections_visualizer = ConnectionsVisualizer( self.other_node_alternatives_contents, self.alternatives_grid_layout, self.resource_database, requirement, False)
def configurable_node_assignment( self, configuration: DreadConfiguration, game: GameDescription, rng: Random) -> Iterator[NodeConfigurationAssociation]: result = [] rsb = game.resource_database requirement_for_type = { "POWERBEAM": rsb.requirement_template["Shoot Beam"], "BOMB": rsb.requirement_template["Lay Bomb"], "MISSILE": rsb.requirement_template["Shoot Missile"], "SUPERMISSILE": rsb.requirement_template["Shoot Super Missile"], "POWERBOMB": rsb.requirement_template["Lay Power Bomb"], "SCREWATTACK": ResourceRequirement.simple(rsb.get_item("Screw")), "WEIGHT": Requirement.impossible(), "SPEEDBOOST": ResourceRequirement.simple(rsb.get_item("Speed")), } for node in game.world_list.iterate_nodes(): if not isinstance(node, ConfigurableNode): continue result.append((game.world_list.identifier_for_node(node), RequirementAnd([ requirement_for_type[block_type] for block_type in node.extra["tile_types"] ]).simplify())) return result
def test_connections_from_dock_blast_shield(empty_patches): # Setup trivial = Requirement.trivial() req_1 = ResourceRequirement.simple( SimpleResourceInfo(0, "Ev1", "Ev1", ResourceType.EVENT)) req_2 = ResourceRequirement.simple( SimpleResourceInfo(1, "Ev2", "Ev2", ResourceType.EVENT)) dock_type = DockType("Type", "Type", frozendict()) weak_1 = DockWeakness(0, "Weak 1", frozendict(), req_1, None) weak_2 = DockWeakness(1, "Weak 2", frozendict(), trivial, DockLock(DockLockType.FRONT_BLAST_BACK_BLAST, req_2)) node_1_identifier = NodeIdentifier.create("W", "Area 1", "Node 1") node_2_identifier = NodeIdentifier.create("W", "Area 2", "Node 2") node_1 = DockNode(node_1_identifier, 0, False, None, "", ("default", ), {}, dock_type, node_2_identifier, weak_1, None, None) node_1_lock = DockLockNode.create_from_dock(node_1, 1) node_2 = DockNode(node_2_identifier, 2, False, None, "", ("default", ), {}, dock_type, node_1_identifier, weak_2, None, None) node_2_lock = DockLockNode.create_from_dock(node_2, 3) area_1 = Area("Area 1", None, True, [node_1, node_1_lock], {}, {}) area_2 = Area("Area 2", None, True, [node_2, node_2_lock], {}, {}) world = World("W", [area_1, area_2], {}) world_list = WorldList([world]) world_list.ensure_has_node_cache() game_mock = MagicMock() game_mock.world_list = world_list patches = dataclasses.replace(empty_patches, game=game_mock) context = NodeContext( patches=patches, current_resources=ResourceCollection(), database=patches.game.resource_database, node_provider=world_list, ) # Run result_1 = list(node_1.connections_from(context)) result_2 = list(node_2.connections_from(context)) # Assert simple = ResourceRequirement.simple assert result_1 == [ (node_2, RequirementAnd( [req_1, simple(NodeResourceInfo.from_node(node_2, context))])), (node_1_lock, RequirementAnd([trivial, req_2])), ] assert result_2 == [ (node_1, simple(NodeResourceInfo.from_node(node_2, context))), (node_2_lock, req_2), ]
def find_invalid_strongly_connected_components(game: GameDescription) -> Iterator[str]: import networkx graph = networkx.DiGraph() for node in game.world_list.iterate_nodes(): if isinstance(node, DockLockNode): continue graph.add_node(node) context = NodeContext( patches=GamePatches.create_from_game(game, 0, None), current_resources=ResourceCollection.with_database(game.resource_database), database=game.resource_database, node_provider=game.world_list, ) for node in game.world_list.iterate_nodes(): if node not in graph: continue for other, req in game.world_list.potential_nodes_from(node, context): if other not in graph: continue if req != Requirement.impossible(): graph.add_edge(node, other) starting_node = game.world_list.resolve_teleporter_connection(game.starting_location) for strong_comp in networkx.strongly_connected_components(graph): components: set[Node] = strong_comp # The starting location determines the default component if starting_node in components: continue if any(node.extra.get("different_strongly_connected_component", False) for node in components): continue if len(components) == 1: node = next(iter(components)) # If the component is a single node which is the default node of it's area, allow it area = game.world_list.nodes_to_area(node) if area.default_node == node.name: continue # We accept nodes that have no paths out or in. if not graph.in_edges(node) and not graph.edges(node): continue names = sorted( game.world_list.node_name(node, with_world=True) for node in strong_comp ) yield "Unknown strongly connected component detected containing {} nodes:\n{}".format(len(names), names)
def pretty_print_requirement(requirement: Requirement, level: int = 0) -> Iterator[Tuple[int, str]]: if requirement == Requirement.impossible(): yield level, "Impossible" elif requirement == Requirement.trivial(): yield level, "Trivial" elif isinstance(requirement, RequirementArrayBase): yield from pretty_print_requirement_array(requirement, level) elif isinstance(requirement, ResourceRequirement): yield level, pretty_print_resource_requirement(requirement) elif isinstance(requirement, RequirementTemplate): yield level, requirement.template_name else: raise RuntimeError( f"Unknown requirement type: {type(requirement)} - {requirement}")
def connections_from( self, context: NodeContext) -> typing.Iterator[tuple[Node, Requirement]]: target_area_identifier = context.patches.get_elevator_connection_for( self) if target_area_identifier is None: return yield context.node_provider.default_node_for_area( target_area_identifier), Requirement.trivial()
def create_new_template(self): template_name, did_confirm = QtWidgets.QInputDialog.getText( self, "New Template", "Insert template name:") if not did_confirm or template_name == "": return self.db.requirement_template[template_name] = Requirement.trivial() self.create_template_editor(template_name) self.tab_template_layout.removeWidget(self.create_new_template_button) self.tab_template_layout.addWidget(self.create_new_template_button)
def replace_connection_with(self, target_node: Node, requirement: Requirement): current_node = self.current_node if requirement == Requirement.impossible(): requirement = None self.editor.edit_connections(self.current_area, current_node, target_node, requirement) self.update_connections() self.area_view_canvas.update()
async def _open_edit_connection(self): if self._check_for_edit_dialog(): return from_node = self.current_node target_node = self.current_connection_node assert from_node is not None assert target_node is not None requirement = self.current_area.connections[from_node].get( target_node, Requirement.impossible()) editor = ConnectionsEditor(self, self.resource_database, requirement) if await self._execute_edit_dialog(editor): self.editor.edit_connections(self.current_area, from_node, target_node, editor.final_requirement) self.update_connections() self.area_view_canvas.update()
def satisfiable_actions(self, state: State, victory_condition: Requirement, ) -> Iterator[Tuple[ResourceNode, int]]: interesting_resources = calculate_interesting_resources( self._satisfiable_requirements.union(victory_condition.as_set(state.resource_database).alternatives), state.resources, state.energy, state.resource_database) # print(" > satisfiable actions, with {} interesting resources".format(len(interesting_resources))) for action, energy in self.possible_actions(state): for resource, amount in action.resource_gain_on_collect(state.node_context()): if resource in interesting_resources: yield action, energy break
def _lock_connection(self, context: NodeContext) -> typing.Optional[tuple[Node, Requirement]]: requirement = Requirement.trivial() forward_weakness = self.get_front_weakness(context) forward_lock = forward_weakness.lock if forward_lock is not None: requirement = self._get_lock_requirement(context, forward_weakness) back_weakness = self.get_back_weakness(context) back_lock = None if back_weakness is not None: back_lock = back_weakness.lock if back_lock is not None and back_lock.lock_type == DockLockType.FRONT_BLAST_BACK_BLAST: requirement = RequirementAnd([requirement, back_lock.requirement]) if forward_lock is None and back_lock is None: return None return self.lock_node, requirement
def _potential_nodes_from( self, node: Node) -> Iterator[Tuple[Node, RequirementSet]]: extra_requirement = _extra_requirement_for_node( self._game, self.node_context(), node) requirement_to_leave = node.requirement_to_leave( self._state.node_context()) for target_node, requirement in self._game.world_list.potential_nodes_from( node, self.node_context()): if target_node is None: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd( [requirement, requirement_to_leave]) if extra_requirement is not None: requirement = RequirementAnd([requirement, extra_requirement]) yield target_node, requirement.as_set( self._state.resource_database)
def state_for_current_configuration(self) -> Optional[State]: state = self._initial_state.copy() if self._actions: state.node = self._actions[-1] state.patches = state.patches.assign_elevators( (state.world_list.get_teleporter_node(teleporter), combo.currentData()) for teleporter, combo in self._elevator_id_to_combo.items()) for gate, item in self._translator_gate_to_combo.items(): scan_visor = self.game_description.resource_database.get_item( "Scan") requirement: Optional[ LayoutTranslatorRequirement] = item.currentData() if requirement is None: translator_req = Requirement.impossible() else: translator = self.game_description.resource_database.get_item( requirement.item_name) translator_req = ResourceRequirement.simple(translator) state.patches.configurable_nodes[gate] = RequirementAnd([ ResourceRequirement.simple(scan_visor), translator_req, ]) for pickup, quantity in self._collected_pickups.items(): for _ in range(quantity): add_pickup_to_state(state, pickup) for node in self._collected_nodes: state.resources.add_resource_gain( node.resource_gain_on_collect(state.node_context())) return state
def base_dock_name_raw(dock_type: DockType, weakness: DockWeakness, connection: AreaIdentifier) -> str: expected_connector = "to" if weakness.requirement == Requirement.impossible() and weakness.name != "Not Determined": expected_connector = "from" return f"{dock_type.long_name} {expected_connector} {connection.area_name}"
def test_simplified_requirement(database): def _req(name: str): id_req = ResourceRequirement.simple(database.get_item(name)) return id_req test_parameters = [ ( RequirementOr([ RequirementAnd([ _req("A"), ]), ]), _req("A"), ), ( RequirementAnd([ RequirementOr([ _req("A"), ]), ]), _req("A"), ), ( RequirementAnd([ RequirementOr([_req("B"), Requirement.trivial()]), RequirementAnd([ Requirement.trivial(), _req("A"), ]), ]), _req("A"), ), ( RequirementOr([ _req("A"), RequirementAnd([_req("B"), _req("C")]), RequirementAnd([_req("B"), _req("D")]), ]), RequirementOr([ _req("A"), RequirementAnd([ _req("B"), RequirementOr([ _req("C"), _req("D"), ]), ]), ]), ), ( RequirementOr([ RequirementAnd([_req("B"), _req("C")]), RequirementAnd([_req("B"), _req("D")]), ]), RequirementAnd([ _req("B"), RequirementOr([ _req("C"), _req("D"), ]), ]), ), ( RequirementOr([ _req("A"), _req("A"), ]), _req("A"), ), ( RequirementAnd([ _req("A"), _req("A"), ]), _req("A"), ), ( RequirementOr([ RequirementAnd([ _req("A"), RequirementOr([_req("A"), RequirementOr([_req("A")])]) ]), RequirementAnd([ _req("A"), RequirementOr([ _req("A"), RequirementOr([]), ]), ]), ]), _req("A"), ), ( RequirementOr([ RequirementAnd([ _req("A"), RequirementOr([_req("A"), RequirementOr([_req("A")])]) ]), RequirementAnd([ _req("A"), RequirementOr([ _req("A"), RequirementOr([_req("A")]), ]) ]) ]), _req("A"), ) ] for original, expected in test_parameters: simplified = original.simplify() assert simplified == expected assert simplified.as_set(database) == expected.as_set(database)
class NodeDetailsPopup(QtWidgets.QDialog, Ui_NodeDetailsPopup): _unlocked_by_requirement = Requirement.trivial() _connections_visualizer = None def __init__(self, game: GameDescription, node: Node): super().__init__() self.setupUi(self) common_qt_lib.set_default_window_icon(self) self.game = game self.node = node self._type_to_tab = { GenericNode: self.tab_generic, DockNode: self.tab_dock, PickupNode: self.tab_pickup, TeleporterNode: self.tab_teleporter, EventNode: self.tab_event, ConfigurableNode: self.tab_translator_gate, LogbookNode: self.tab_logbook, PlayerShipNode: self.tab_player_ship, } tab_to_type = {tab: node_type for node_type, tab in self._type_to_tab.items()} # Dynamic Stuff for i, node_type in enumerate(self._type_to_tab.keys()): self.node_type_combo.setItemData(i, node_type) self.layers_combo.clear() for layer in game.layers: self.layers_combo.addItem(layer) self.dock_type_combo.clear() for i, dock_type in enumerate(game.dock_weakness_database.dock_types): self.dock_type_combo.addItem(dock_type.long_name, userData=dock_type) refresh_if_needed(self.dock_type_combo, self.on_dock_type_combo) for world in sorted(game.world_list.worlds, key=lambda x: x.name): self.dock_connection_world_combo.addItem(world.name, userData=world) self.teleporter_destination_world_combo.addItem(world.name, userData=world) refresh_if_needed(self.teleporter_destination_world_combo, self.on_dock_connection_world_combo) refresh_if_needed(self.teleporter_destination_world_combo, self.on_teleporter_destination_world_combo) for event in sorted(game.resource_database.event, key=lambda it: it.long_name): self.event_resource_combo.addItem(event.long_name, event) if self.event_resource_combo.count() == 0: self.event_resource_combo.addItem("No events in database", None) self.event_resource_combo.setEnabled(False) for i, dock_type in enumerate(enum_lib.iterate_enum(LoreType)): self.lore_type_combo.setItemData(i, dock_type) refresh_if_needed(self.lore_type_combo, self.on_lore_type_combo) self.set_unlocked_by(Requirement.trivial()) # Signals self.button_box.accepted.connect(self.try_accept) self.button_box.rejected.connect(self.reject) self.name_edit.textEdited.connect(self.on_name_edit) self.node_type_combo.currentIndexChanged.connect(self.on_node_type_combo) self.dock_connection_world_combo.currentIndexChanged.connect(self.on_dock_connection_world_combo) self.dock_connection_area_combo.currentIndexChanged.connect(self.on_dock_connection_area_combo) self.dock_type_combo.currentIndexChanged.connect(self.on_dock_type_combo) self.dock_update_name_button.clicked.connect(self.on_dock_update_name_button) self.teleporter_destination_world_combo.currentIndexChanged.connect(self.on_teleporter_destination_world_combo) self.lore_type_combo.currentIndexChanged.connect(self.on_lore_type_combo) self.player_ship_unlocked_button.clicked.connect(self.on_player_ship_unlocked_button) # Hide the tab bar tab_bar: QtWidgets.QTabBar = self.tab_widget.findChild(QtWidgets.QTabBar) tab_bar.hide() # Values self.name_edit.setText(node.name) self.heals_check.setChecked(node.heal) self.location_group.setChecked(node.location is not None) if node.location is not None: self.location_x_spin.setValue(node.location.x) self.location_y_spin.setValue(node.location.y) self.location_z_spin.setValue(node.location.z) self.description_edit.setMarkdown(node.description) self.extra_edit.setPlainText(json.dumps(frozen_lib.unwrap(node.extra), indent=4)) try: visible_tab = self._fill_for_type(node) self.node_type_combo.setCurrentIndex(self.node_type_combo.findData(tab_to_type[visible_tab])) refresh_if_needed(self.node_type_combo, self.on_node_type_combo) except Exception: pass self.on_name_edit(self.name_edit.text()) def _fill_for_type(self, node: Node) -> QtWidgets.QWidget: if isinstance(node, GenericNode): return self.tab_generic elif isinstance(node, DockNode): self.fill_for_dock(node) return self.tab_dock elif isinstance(node, PickupNode): self.fill_for_pickup(node) return self.tab_pickup elif isinstance(node, TeleporterNode): self.fill_for_teleporter(node) return self.tab_teleporter elif isinstance(node, EventNode): self.fill_for_event(node) return self.tab_event elif isinstance(node, ConfigurableNode): self.fill_for_translator_gate(node) return self.tab_translator_gate elif isinstance(node, LogbookNode): self.fill_for_logbook_node(node) return self.tab_logbook elif isinstance(node, PlayerShipNode): self.fill_for_player_ship_node(node) return self.tab_player_ship else: raise ValueError(f"Unknown node type: {node}") def fill_for_dock(self, node: DockNode): # Connection other_node = self.game.world_list.node_by_identifier(node.default_connection) area = self.game.world_list.nodes_to_area(other_node) world = self.game.world_list.nodes_to_world(other_node) self.dock_connection_world_combo.setCurrentIndex(self.dock_connection_world_combo.findData(world)) refresh_if_needed(self.dock_connection_world_combo, self.on_dock_connection_world_combo) self.dock_connection_area_combo.setCurrentIndex(self.dock_connection_area_combo.findData(area)) refresh_if_needed(self.dock_connection_area_combo, self.on_dock_connection_area_combo) self.dock_connection_node_combo.setCurrentIndex(self.dock_connection_node_combo.findData(other_node)) # Dock Weakness self.dock_type_combo.setCurrentIndex(self.dock_type_combo.findData(node.dock_type)) refresh_if_needed(self.dock_type_combo, self.on_dock_type_combo) self.dock_weakness_combo.setCurrentIndex(self.dock_weakness_combo.findData(node.default_dock_weakness)) def fill_for_pickup(self, node: PickupNode): self.pickup_index_spin.setValue(node.pickup_index.index) self.major_location_check.setChecked(node.major_location) def fill_for_teleporter(self, node: TeleporterNode): world = self.game.world_list.world_by_area_location(node.default_connection) try: area = self.game.world_list.area_by_area_location(node.default_connection) except KeyError: area = None self.teleporter_destination_world_combo.setCurrentIndex(self.teleporter_destination_world_combo.findData(world)) refresh_if_needed(self.teleporter_destination_world_combo, self.on_teleporter_destination_world_combo) self.teleporter_destination_area_combo.setCurrentIndex(self.teleporter_destination_area_combo.findData(area)) self.teleporter_editable_check.setChecked(node.editable) self.teleporter_vanilla_name_edit.setChecked(node.keep_name_when_vanilla) def fill_for_event(self, node: EventNode): self.event_resource_combo.setCurrentIndex(self.event_resource_combo.findData(node.event)) def fill_for_translator_gate(self, node: ConfigurableNode): pass def fill_for_logbook_node(self, node: LogbookNode): self.logbook_string_asset_id_edit.setText(hex(node.string_asset_id).upper()) self.lore_type_combo.setCurrentIndex(self.lore_type_combo.findData(node.lore_type)) refresh_if_needed(self.lore_type_combo, self.on_lore_type_combo) if node.lore_type == LoreType.REQUIRES_ITEM: self.logbook_extra_combo.setCurrentIndex(self.logbook_extra_combo.findData(node.required_translator)) elif node.lore_type == LoreType.SPECIFIC_PICKUP: self.logbook_extra_combo.setCurrentIndex(self.logbook_extra_combo.findData(node.hint_index)) def fill_for_player_ship_node(self, node: PlayerShipNode): self.set_unlocked_by(node.is_unlocked) def set_unlocked_by(self, requirement: Requirement): if self._connections_visualizer is not None: self._connections_visualizer.deleteLater() self._connections_visualizer = None self._unlocked_by_requirement = requirement self._connections_visualizer = ConnectionsVisualizer( self.player_ship_unlocked_group, self.player_ship_unlocked_layout, self.game.resource_database, requirement, False ) # Signals def on_name_edit(self, value: str): has_error = False try: new_node = self.create_new_node() except ValueError: new_node = None if isinstance(new_node, DockNode): area = self.game.world_list.nodes_to_area(self.node) has_error = not integrity_check.dock_has_correct_name(area, new_node)[0] common_qt_lib.set_error_border_stylesheet(self.name_edit, has_error) def on_node_type_combo(self, _): self.tab_widget.setCurrentWidget(self._type_to_tab[self.node_type_combo.currentData()]) def on_dock_connection_world_combo(self, _): world: World = self.dock_connection_world_combo.currentData() self.dock_connection_area_combo.clear() for area in sorted(world.areas, key=lambda x: x.name): self.dock_connection_area_combo.addItem(area.name, userData=area) def on_dock_connection_area_combo(self, _): area: Optional[Area] = self.dock_connection_area_combo.currentData() self.dock_connection_node_combo.clear() empty = True if area is not None: for node in area.nodes: if isinstance(node, DockNode): self.dock_connection_node_combo.addItem(node.name, userData=node) empty = False if empty: self.dock_connection_node_combo.addItem("Other", None) def on_dock_type_combo(self, _): self.dock_weakness_combo.clear() for weakness in self.game.dock_weakness_database.get_by_type(self.dock_type_combo.currentData()): self.dock_weakness_combo.addItem(weakness.name, weakness) def on_dock_update_name_button(self): new_node = self.create_new_node() assert isinstance(new_node, DockNode) expected_name = integrity_check.base_dock_name(new_node) self.name_edit.setText(expected_name) self.on_name_edit(self.name_edit.text()) def on_teleporter_destination_world_combo(self, _): world: World = self.teleporter_destination_world_combo.currentData() self.teleporter_destination_area_combo.clear() for area in sorted(world.areas, key=lambda x: x.name): self.teleporter_destination_area_combo.addItem(area.name, userData=area) def on_lore_type_combo(self, _): lore_type: LoreType = self.lore_type_combo.currentData() self.logbook_extra_combo.clear() if lore_type == LoreType.REQUIRES_ITEM: self.logbook_extra_label.setText("Translator needed:") for item in self.game.resource_database.item: self.logbook_extra_combo.addItem(item.long_name, item) elif lore_type == LoreType.SPECIFIC_PICKUP: self.logbook_extra_label.setText("Pickup index hinted:") for node in self.game.world_list.iterate_nodes(): if isinstance(node, PickupNode): self.logbook_extra_combo.addItem("{} - {}".format( self.game.world_list.node_name(node, True, True), node.pickup_index.index, ), node.pickup_index.index) else: self.logbook_extra_label.setText("Nothing:") self.logbook_extra_combo.addItem("Nothing", None) @asyncSlot() async def on_player_ship_unlocked_button(self): self._edit_popup = ConnectionsEditor(self, self.game.resource_database, self._unlocked_by_requirement) self._edit_popup.setModal(True) try: result = await async_dialog.execute_dialog(self._edit_popup) if result == QtWidgets.QDialog.Accepted: self.set_unlocked_by(self._edit_popup.final_requirement) finally: self._edit_popup = None # Final def create_new_node(self) -> Node: node_type = self.node_type_combo.currentData() identifier = self.node.identifier.renamed(self.name_edit.text()) node_index = self.node.node_index heal = self.heals_check.isChecked() location = None if self.location_group.isChecked(): location = NodeLocation(self.location_x_spin.value(), self.location_y_spin.value(), self.location_z_spin.value()) description = self.description_edit.toMarkdown() extra = json.loads(self.extra_edit.toPlainText()) layers = (self.layers_combo.currentText(),) if node_type == GenericNode: return GenericNode(identifier, node_index, heal, location, description, layers, extra) elif node_type == DockNode: connection_node: Node = self.dock_connection_node_combo.currentData() return DockNode( identifier, node_index, heal, location, description, layers, extra, self.dock_type_combo.currentData(), self.game.world_list.identifier_for_node(connection_node), self.dock_weakness_combo.currentData(), None, None, ) elif node_type == PickupNode: return PickupNode( identifier, node_index, heal, location, description, layers, extra, PickupIndex(self.pickup_index_spin.value()), self.major_location_check.isChecked(), ) elif node_type == TeleporterNode: dest_world: World = self.teleporter_destination_world_combo.currentData() dest_area: Area = self.teleporter_destination_area_combo.currentData() return TeleporterNode( identifier, node_index, heal, location, description, layers, extra, AreaIdentifier( world_name=dest_world.name, area_name=dest_area.name, ), self.teleporter_vanilla_name_edit.isChecked(), self.teleporter_editable_check.isChecked(), ) elif node_type == EventNode: event = self.event_resource_combo.currentData() if event is None: raise ValueError("There are no events in the database, unable to create EventNode.") return EventNode( identifier, node_index, heal, location, description, layers, extra, event, ) elif node_type == ConfigurableNode: return ConfigurableNode( identifier, node_index, heal, location, description, layers, extra, ) elif node_type == LogbookNode: lore_type: LoreType = self.lore_type_combo.currentData() if lore_type == LoreType.REQUIRES_ITEM: required_translator = self.logbook_extra_combo.currentData() if required_translator is None: raise ValueError("Missing required translator.") else: required_translator = None if lore_type == LoreType.SPECIFIC_PICKUP: hint_index = self.logbook_extra_combo.currentData() else: hint_index = None return LogbookNode( identifier, node_index, heal, location, description, layers, extra, int(self.logbook_string_asset_id_edit.text(), 0), self._get_scan_visor(), lore_type, required_translator, hint_index ) elif node_type == PlayerShipNode: return PlayerShipNode( identifier, node_index, heal, location, description, layers, extra, self._unlocked_by_requirement, self._get_command_visor() ) else: raise RuntimeError(f"Unknown node type: {node_type}") def _get_scan_visor(self): try: return find_resource_info_with_long_name( self.game.resource_database.item, "Scan Visor" ) except MissingResource: return None def _get_command_visor(self): try: return find_resource_info_with_long_name( self.game.resource_database.item, "Command Visor" ) except MissingResource: return None def try_accept(self): try: self.create_new_node() self.accept() except Exception as e: logging.exception(f"Unable to save node: {e}") box = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Warning, "Invalid configuration", f"Unable to save node: {e}", QtWidgets.QMessageBox.Ok, None, ) box.setDefaultButton(QtWidgets.QMessageBox.Ok) box.setDetailedText("".join(traceback.format_tb(e.__traceback__))) common_qt_lib.set_default_window_icon(box) box.exec_()
def __init__(self, game: GameDescription, node: Node): super().__init__() self.setupUi(self) common_qt_lib.set_default_window_icon(self) self.game = game self.node = node self._type_to_tab = { GenericNode: self.tab_generic, DockNode: self.tab_dock, PickupNode: self.tab_pickup, TeleporterNode: self.tab_teleporter, EventNode: self.tab_event, ConfigurableNode: self.tab_translator_gate, LogbookNode: self.tab_logbook, PlayerShipNode: self.tab_player_ship, } tab_to_type = {tab: node_type for node_type, tab in self._type_to_tab.items()} # Dynamic Stuff for i, node_type in enumerate(self._type_to_tab.keys()): self.node_type_combo.setItemData(i, node_type) self.layers_combo.clear() for layer in game.layers: self.layers_combo.addItem(layer) self.dock_type_combo.clear() for i, dock_type in enumerate(game.dock_weakness_database.dock_types): self.dock_type_combo.addItem(dock_type.long_name, userData=dock_type) refresh_if_needed(self.dock_type_combo, self.on_dock_type_combo) for world in sorted(game.world_list.worlds, key=lambda x: x.name): self.dock_connection_world_combo.addItem(world.name, userData=world) self.teleporter_destination_world_combo.addItem(world.name, userData=world) refresh_if_needed(self.teleporter_destination_world_combo, self.on_dock_connection_world_combo) refresh_if_needed(self.teleporter_destination_world_combo, self.on_teleporter_destination_world_combo) for event in sorted(game.resource_database.event, key=lambda it: it.long_name): self.event_resource_combo.addItem(event.long_name, event) if self.event_resource_combo.count() == 0: self.event_resource_combo.addItem("No events in database", None) self.event_resource_combo.setEnabled(False) for i, dock_type in enumerate(enum_lib.iterate_enum(LoreType)): self.lore_type_combo.setItemData(i, dock_type) refresh_if_needed(self.lore_type_combo, self.on_lore_type_combo) self.set_unlocked_by(Requirement.trivial()) # Signals self.button_box.accepted.connect(self.try_accept) self.button_box.rejected.connect(self.reject) self.name_edit.textEdited.connect(self.on_name_edit) self.node_type_combo.currentIndexChanged.connect(self.on_node_type_combo) self.dock_connection_world_combo.currentIndexChanged.connect(self.on_dock_connection_world_combo) self.dock_connection_area_combo.currentIndexChanged.connect(self.on_dock_connection_area_combo) self.dock_type_combo.currentIndexChanged.connect(self.on_dock_type_combo) self.dock_update_name_button.clicked.connect(self.on_dock_update_name_button) self.teleporter_destination_world_combo.currentIndexChanged.connect(self.on_teleporter_destination_world_combo) self.lore_type_combo.currentIndexChanged.connect(self.on_lore_type_combo) self.player_ship_unlocked_button.clicked.connect(self.on_player_ship_unlocked_button) # Hide the tab bar tab_bar: QtWidgets.QTabBar = self.tab_widget.findChild(QtWidgets.QTabBar) tab_bar.hide() # Values self.name_edit.setText(node.name) self.heals_check.setChecked(node.heal) self.location_group.setChecked(node.location is not None) if node.location is not None: self.location_x_spin.setValue(node.location.x) self.location_y_spin.setValue(node.location.y) self.location_z_spin.setValue(node.location.z) self.description_edit.setMarkdown(node.description) self.extra_edit.setPlainText(json.dumps(frozen_lib.unwrap(node.extra), indent=4)) try: visible_tab = self._fill_for_type(node) self.node_type_combo.setCurrentIndex(self.node_type_combo.findData(tab_to_type[visible_tab])) refresh_if_needed(self.node_type_combo, self.on_node_type_combo) except Exception: pass self.on_name_edit(self.name_edit.text())
def connections_from( self, context: NodeContext) -> Iterator[tuple[Node, Requirement]]: dock = self.dock if dock._lock_connection(context) is not None: yield dock, Requirement.trivial()
def calculate_reach(cls, logic: Logic, initial_state: State) -> "ResolverReach": all_nodes = logic.game.world_list.all_nodes checked_nodes: Dict[int, int] = {} database = initial_state.resource_database context = initial_state.node_context() # Keys: nodes to check # Value: how much energy was available when visiting that node nodes_to_check: Dict[int, int] = { initial_state.node.node_index: initial_state.energy } reach_nodes: Dict[int, int] = {} requirements_by_node: Dict[int, Set[RequirementList]] = defaultdict(set) path_to_node: dict[int, list[int]] = { initial_state.node.node_index: [], } while nodes_to_check: node_index = next(iter(nodes_to_check)) node = all_nodes[node_index] energy = nodes_to_check.pop(node_index) if node.heal: energy = initial_state.maximum_energy checked_nodes[node_index] = energy if node_index != initial_state.node.node_index: reach_nodes[node_index] = energy requirement_to_leave = node.requirement_to_leave(context) for target_node, requirement in logic.game.world_list.potential_nodes_from(node, context): target_node_index = target_node.node_index if checked_nodes.get(target_node_index, math.inf) <= energy or nodes_to_check.get(target_node_index, math.inf) <= energy: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd([requirement, requirement_to_leave]) # Check if the normal requirements to reach that node is satisfied satisfied = requirement.satisfied(initial_state.resources, energy, database) if satisfied: # If it is, check if we additional requirements figured out by backtracking is satisfied satisfied = logic.get_additional_requirements(node).satisfied(initial_state.resources, energy, initial_state.resource_database) if satisfied: nodes_to_check[target_node_index] = energy - requirement.damage(initial_state.resources, database) path_to_node[target_node_index] = list(path_to_node[node_index]) path_to_node[target_node_index].append(node_index) elif target_node: # If we can't go to this node, store the reason in order to build the satisfiable requirements. # Note we ignore the 'additional requirements' here because it'll be added on the end. requirements_by_node[target_node_index].update( requirement.as_set(initial_state.resource_database).alternatives) # Discard satisfiable requirements of nodes reachable by other means for node_index in set(reach_nodes.keys()).intersection(requirements_by_node.keys()): requirements_by_node.pop(node_index) if requirements_by_node: satisfiable_requirements = frozenset.union( *[RequirementSet(requirements).union(logic.get_additional_requirements(all_nodes[node_index])).alternatives for node_index, requirements in requirements_by_node.items()]) else: satisfiable_requirements = frozenset() return ResolverReach(reach_nodes, path_to_node, satisfiable_requirements, logic)
def requirement_to_leave(self, context: NodeContext) -> Requirement: return Requirement.trivial()
def final_requirement(self) -> Optional[Requirement]: result = self.build_requirement() if result == Requirement.impossible(): return None return result
def _uses_trick(requirements: Requirement) -> bool: return any( criteria(individual) for individual in requirements.as_set(database).all_individual)
def is_connection_visible(self, requirement: Requirement) -> bool: return self.state is None or requirement.satisfied( self.state.resources, self.state.energy, self.state.resource_database)
def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None: local_pos = QPointF(self.mapFromGlobal(event.globalPos())) local_pos -= self.get_area_canvas_offset() self._next_node_location = self.qt_local_to_game_loc(local_pos) menu = QtWidgets.QMenu(self) if self.state is None: menu.addAction(self._show_all_connections_action) if self.edit_mode: menu.addAction(self._create_node_action) menu.addAction(self._move_node_action) self._move_node_action.setEnabled( self.highlighted_node is not None) if self.highlighted_node is not None: self._move_node_action.setText( f"Move {self.highlighted_node.name} here") # Areas Menu menu.addSeparator() areas_at_mouse = self._other_areas_at_position(local_pos) for area in areas_at_mouse: sub_menu = QtWidgets.QMenu(f"Area: {area.name}", self) sub_menu.addAction("View area").triggered.connect( functools.partial(self.SelectAreaRequest.emit, area)) if self.edit_mode: sub_menu.addAction( "Create dock here to this area").triggered.connect( functools.partial(self.CreateDockRequest.emit, self._next_node_location, area)) menu.addMenu(sub_menu) if not areas_at_mouse: sub_menu = QtGui.QAction("No areas here", self) sub_menu.setEnabled(False) menu.addAction(sub_menu) # Nodes Menu menu.addSeparator() nodes_at_mouse = self._nodes_at_position(local_pos) if self.highlighted_node in nodes_at_mouse: nodes_at_mouse.remove(self.highlighted_node) for node in nodes_at_mouse: if len(nodes_at_mouse) == 1: menu.addAction(node.name).setEnabled(False) sub_menu = menu else: sub_menu = QtWidgets.QMenu(node.name, self) sub_menu.addAction("Highlight this").triggered.connect( functools.partial(self.SelectNodeRequest.emit, node)) view_connections = sub_menu.addAction("View connections to this") view_connections.setEnabled( (self.edit_mode and self.highlighted_node != node) or (node in self.area.connections.get(self.highlighted_node, {}))) view_connections.triggered.connect( functools.partial(self.SelectConnectionsRequest.emit, node)) if self.edit_mode: sub_menu.addSeparator() sub_menu.addAction( "Replace connection with Trivial").triggered.connect( functools.partial( self.ReplaceConnectionsRequest.emit, node, Requirement.trivial(), )) sub_menu.addAction("Remove connection").triggered.connect( functools.partial( self.ReplaceConnectionsRequest.emit, node, Requirement.impossible(), )) if areas_at_mouse: move_menu = QtWidgets.QMenu("Move to...", self) for area in areas_at_mouse: move_menu.addAction(area.name).triggered.connect( functools.partial( self.MoveNodeToAreaRequest.emit, node, area, )) sub_menu.addMenu(move_menu) if sub_menu != menu: menu.addMenu(sub_menu) if not nodes_at_mouse: sub_menu = QtGui.QAction("No other nodes here", self) sub_menu.setEnabled(False) menu.addAction(sub_menu) # Done menu.exec_(event.globalPos())
def requirement_to_leave(self, context: NodeContext) -> Requirement: if context.current_resources.add_self_as_requirement_to_resources: return ResourceRequirement.simple(self.event) else: return Requirement.trivial()