From 1e115db5485f2e1f843d9b0a9ecbf4766ae6cb71 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 15:59:29 +0000 Subject: [PATCH] feat(ui): add armor set and mindforce implant selectors - Created ArmorSetSelectorDialog for browsing full armor sets - Created MindforceImplantSelectorDialog for MF chips/implants - Both dialogs integrate with Entropia Nexus API - Filter by type, search, and preview functionality included --- ui/armor_set_selector.py | 243 ++++++++++++++++++++++++++++++++++++ ui/mindforce_selector.py | 260 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 ui/armor_set_selector.py create mode 100644 ui/mindforce_selector.py diff --git a/ui/armor_set_selector.py b/ui/armor_set_selector.py new file mode 100644 index 0000000..59d1642 --- /dev/null +++ b/ui/armor_set_selector.py @@ -0,0 +1,243 @@ +""" +Armor Set Selector for Lemontropia Suite +Browse and select full armor sets from Entropia Nexus API +""" + +from decimal import Decimal +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, + QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, + QProgressBar, QGroupBox, QFormLayout +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtGui import QColor +from typing import Optional, List + +from core.nexus_full_api import get_nexus_api, NexusArmorSet + + +class ArmorSetLoaderThread(QThread): + """Background thread for loading armor sets from API.""" + armor_sets_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = get_nexus_api() + armor_sets = api.get_all_armor_sets() + self.armor_sets_loaded.emit(armor_sets) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class ArmorSetSelectorDialog(QDialog): + """Dialog for selecting full armor sets from Entropia Nexus API.""" + + armor_set_selected = pyqtSignal(NexusArmorSet) + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Select Armor Set - Entropia Nexus") + self.setMinimumSize(900, 600) + + self.all_armor_sets: List[NexusArmorSet] = [] + self.selected_armor_set: Optional[NexusArmorSet] = None + + self._setup_ui() + self._load_data() + + def _setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Status + self.status_label = QLabel("Loading armor sets from Entropia Nexus...") + layout.addWidget(self.status_label) + + self.progress = QProgressBar() + self.progress.setRange(0, 0) + layout.addWidget(self.progress) + + # Search + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Search armor sets (e.g., 'Ghost', 'Shogun')...") + self.search_input.textChanged.connect(self._filter_armor_sets) + search_layout.addWidget(self.search_input) + + clear_btn = QPushButton("Clear") + clear_btn.clicked.connect(self.search_input.clear) + search_layout.addWidget(clear_btn) + layout.addLayout(search_layout) + + # Results tree + self.results_tree = QTreeWidget() + self.results_tree.setHeaderLabels([ + "Set Name", "Pieces", "Total Protection", "Set Bonus" + ]) + header = self.results_tree.header() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.results_tree.itemSelectionChanged.connect(self._on_selection_changed) + self.results_tree.itemDoubleClicked.connect(self._on_double_click) + layout.addWidget(self.results_tree) + + # Preview panel + self.preview_group = QGroupBox("Armor Set Preview") + preview_layout = QFormLayout(self.preview_group) + self.preview_name = QLabel("-") + self.preview_pieces = QLabel("-") + self.preview_protection = QLabel("-") + self.preview_bonus = QLabel("-") + preview_layout.addRow("Name:", self.preview_name) + preview_layout.addRow("Pieces:", self.preview_pieces) + preview_layout.addRow("Protection:", self.preview_protection) + preview_layout.addRow("Set Bonus:", self.preview_bonus) + layout.addWidget(self.preview_group) + + # Buttons + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self._on_accept) + buttons.rejected.connect(self.reject) + self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) + self.ok_button.setEnabled(False) + layout.addWidget(buttons) + + def _load_data(self): + """Load armor sets in background thread.""" + self.loader = ArmorSetLoaderThread() + self.loader.armor_sets_loaded.connect(self._on_armor_sets_loaded) + self.loader.error_occurred.connect(self._on_load_error) + self.loader.start() + + def _on_armor_sets_loaded(self, armor_sets: List[NexusArmorSet]): + """Handle loaded armor sets.""" + self.all_armor_sets = armor_sets + self._populate_results(armor_sets) + self.status_label.setText(f"Showing {len(armor_sets)} armor sets") + self.progress.setRange(0, 100) + self.progress.setValue(100) + + def _on_load_error(self, error: str): + """Handle load error.""" + self.status_label.setText(f"Error loading armor sets: {error}") + self.progress.setRange(0, 100) + self.progress.setValue(0) + + def _populate_results(self, armor_sets: List[NexusArmorSet]): + """Populate results tree.""" + self.results_tree.clear() + + if not armor_sets: + item = QTreeWidgetItem() + item.setText(0, "No armor sets available") + item.setForeground(0, QColor("#888888")) + self.results_tree.addTopLevelItem(item) + return + + # Sort by total protection + armor_sets = sorted(armor_sets, key=lambda s: s.total_protection.get_total(), reverse=True) + + for armor_set in armor_sets: + item = QTreeWidgetItem() + item.setText(0, armor_set.name) + item.setText(1, f"{len(armor_set.pieces)} pieces") + + # Format protection + prot = armor_set.total_protection + prot_str = f"Total: {prot.get_total():.1f}" + if prot.impact > 0: + prot_str += f" | Imp:{prot.impact:.0f}" + if prot.cut > 0: + prot_str += f" | Cut:{prot.cut:.0f}" + if prot.stab > 0: + prot_str += f" | Stab:{prot.stab:.0f}" + item.setText(2, prot_str) + + # Set bonus + bonus = armor_set.set_bonus or "None" + item.setText(3, bonus) + + # Color by set bonus presence + if armor_set.set_bonus: + item.setForeground(3, QColor("#4caf50")) + + item.setData(0, Qt.ItemDataRole.UserRole, armor_set) + self.results_tree.addTopLevelItem(item) + + def _filter_armor_sets(self): + """Filter armor sets based on search.""" + search = self.search_input.text().lower() + if not search: + self._populate_results(self.all_armor_sets) + return + + filtered = [ + s for s in self.all_armor_sets + if search in s.name.lower() + or any(search in piece.lower() for piece in s.pieces) + ] + self._populate_results(filtered) + + def _on_selection_changed(self): + """Handle selection change.""" + items = self.results_tree.selectedItems() + if items: + self.selected_armor_set = items[0].data(0, Qt.ItemDataRole.UserRole) + self._update_preview(self.selected_armor_set) + self.ok_button.setEnabled(True) + else: + self.selected_armor_set = None + self.ok_button.setEnabled(False) + + def _update_preview(self, armor_set: NexusArmorSet): + """Update preview panel.""" + self.preview_name.setText(armor_set.name) + self.preview_pieces.setText(", ".join(armor_set.pieces)) + + prot = armor_set.total_protection + prot_str = f"Impact: {prot.impact:.1f}, Cut: {prot.cut:.1f}, Stab: {prot.stab:.1f}\n" + prot_str += f"Burn: {prot.burn:.1f}, Cold: {prot.cold:.1f}, Acid: {prot.acid:.1f}, Elec: {prot.electric:.1f}\n" + prot_str += f"Total: {prot.get_total():.1f}" + self.preview_protection.setText(prot_str) + + self.preview_bonus.setText(armor_set.set_bonus or "None") + if armor_set.set_bonus: + self.preview_bonus.setStyleSheet("color: #4caf50;") + else: + self.preview_bonus.setStyleSheet("") + + def _on_double_click(self, item: QTreeWidgetItem, column: int): + """Handle double click.""" + if item.data(0, Qt.ItemDataRole.UserRole): + self._on_accept() + + def _on_accept(self): + """Handle OK button.""" + if self.selected_armor_set: + self.armor_set_selected.emit(self.selected_armor_set) + self.accept() + + +# Main entry for testing +if __name__ == "__main__": + import sys + import logging + from PyQt6.QtWidgets import QApplication + + logging.basicConfig(level=logging.INFO) + + app = QApplication(sys.argv) + app.setStyle('Fusion') + + dialog = ArmorSetSelectorDialog() + + # Connect signal for testing + dialog.armor_set_selected.connect(lambda s: print(f"Selected armor set: {s.name}")) + + if dialog.exec() == QDialog.DialogCode.Accepted: + print("Armor set selected!") + + sys.exit(0) diff --git a/ui/mindforce_selector.py b/ui/mindforce_selector.py new file mode 100644 index 0000000..adfeefc --- /dev/null +++ b/ui/mindforce_selector.py @@ -0,0 +1,260 @@ +""" +Mindforce Implant Selector for Lemontropia Suite +Browse and select mindforce implants from Entropia Nexus API +""" + +from decimal import Decimal +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, + QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, + QProgressBar, QGroupBox, QFormLayout, QComboBox +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtGui import QColor +from typing import Optional, List + +from core.nexus_full_api import get_nexus_api, NexusMindforceImplant + + +class MindforceImplantLoaderThread(QThread): + """Background thread for loading mindforce implants from API.""" + implants_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = get_nexus_api() + implants = api.get_all_mindforce_implants() + self.implants_loaded.emit(implants) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class MindforceImplantSelectorDialog(QDialog): + """Dialog for selecting mindforce implants from Entropia Nexus API.""" + + implant_selected = pyqtSignal(NexusMindforceImplant) + + def __init__(self, parent=None, implant_type: str = ""): + super().__init__(parent) + self.preferred_type = implant_type.lower() + + type_names = { + "healing": "Healing Chip", + "damage": "Damage Chip", + "utility": "Utility Chip" + } + title_type = type_names.get(self.preferred_type, "Mindforce Implant") + + self.setWindowTitle(f"Select {title_type} - Entropia Nexus") + self.setMinimumSize(800, 500) + + self.all_implants: List[NexusMindforceImplant] = [] + self.selected_implant: Optional[NexusMindforceImplant] = None + + self._setup_ui() + self._load_data() + + def _setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Status + self.status_label = QLabel("Loading mindforce implants from Entropia Nexus...") + layout.addWidget(self.status_label) + + self.progress = QProgressBar() + self.progress.setRange(0, 0) + layout.addWidget(self.progress) + + # Filters + filter_layout = QHBoxLayout() + + filter_layout.addWidget(QLabel("Type:")) + self.type_combo = QComboBox() + self.type_combo.addItems(["All", "Healing", "Damage", "Utility"]) + if self.preferred_type: + type_map = {"healing": 1, "damage": 2, "utility": 3} + self.type_combo.setCurrentIndex(type_map.get(self.preferred_type, 0)) + self.type_combo.currentTextChanged.connect(self._filter_implants) + filter_layout.addWidget(self.type_combo) + + filter_layout.addWidget(QLabel("Search:")) + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Search implants...") + self.search_input.textChanged.connect(self._filter_implants) + filter_layout.addWidget(self.search_input) + + clear_btn = QPushButton("Clear") + clear_btn.clicked.connect(self.search_input.clear) + filter_layout.addWidget(clear_btn) + + layout.addLayout(filter_layout) + + # Results tree + self.results_tree = QTreeWidget() + self.results_tree.setHeaderLabels([ + "Name", "Type", "Chip Type", "Decay (PEC)", "Prof. Level", "Limited" + ]) + header = self.results_tree.header() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.results_tree.itemSelectionChanged.connect(self._on_selection_changed) + self.results_tree.itemDoubleClicked.connect(self._on_double_click) + layout.addWidget(self.results_tree) + + # Preview panel + self.preview_group = QGroupBox("Implant Preview") + preview_layout = QFormLayout(self.preview_group) + self.preview_name = QLabel("-") + self.preview_type = QLabel("-") + self.preview_decay = QLabel("-") + preview_layout.addRow("Name:", self.preview_name) + preview_layout.addRow("Type:", self.preview_type) + preview_layout.addRow("Decay/Use:", self.preview_decay) + layout.addWidget(self.preview_group) + + # Buttons + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self._on_accept) + buttons.rejected.connect(self.reject) + self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) + self.ok_button.setEnabled(False) + layout.addWidget(buttons) + + def _load_data(self): + """Load implants in background thread.""" + self.loader = MindforceImplantLoaderThread() + self.loader.implants_loaded.connect(self._on_implants_loaded) + self.loader.error_occurred.connect(self._on_load_error) + self.loader.start() + + def _on_implants_loaded(self, implants: List[NexusMindforceImplant]): + """Handle loaded implants.""" + self.all_implants = implants + self._filter_implants() + self.status_label.setText(f"Loaded {len(implants)} mindforce implants") + self.progress.setRange(0, 100) + self.progress.setValue(100) + + def _on_load_error(self, error: str): + """Handle load error.""" + self.status_label.setText(f"Error loading implants: {error}") + self.progress.setRange(0, 100) + self.progress.setValue(0) + + def _populate_results(self, implants: List[NexusMindforceImplant]): + """Populate results tree.""" + self.results_tree.clear() + + if not implants: + item = QTreeWidgetItem() + item.setText(0, "No implants available") + item.setForeground(0, QColor("#888888")) + self.results_tree.addTopLevelItem(item) + return + + # Sort by decay (lower is better economy) + implants = sorted(implants, key=lambda i: i.decay) + + for implant in implants: + item = QTreeWidgetItem() + item.setText(0, implant.name) + item.setText(1, implant.implant_type.title()) + item.setText(2, implant.chip_type) + item.setText(3, f"{implant.decay:.2f}") + item.setText(4, str(implant.profession_level) if implant.profession_level > 0 else "-") + item.setText(5, "Yes" if implant.is_limited else "No") + + # Color limited items + if implant.is_limited: + item.setForeground(5, QColor("#ff9800")) + + # Color by type + if implant.implant_type == "healing": + item.setForeground(1, QColor("#4caf50")) + elif implant.implant_type == "damage": + item.setForeground(1, QColor("#f44336")) + + item.setData(0, Qt.ItemDataRole.UserRole, implant) + self.results_tree.addTopLevelItem(item) + + def _filter_implants(self): + """Filter implants based on search and type.""" + type_filter = self.type_combo.currentText().lower() + search = self.search_input.text().lower() + + filtered = self.all_implants + + # Filter by type + if type_filter != "all": + filtered = [i for i in filtered if i.implant_type == type_filter] + + # Filter by search + if search: + filtered = [ + i for i in filtered + if search in i.name.lower() + or search in i.chip_type.lower() + ] + + self._populate_results(filtered) + + def _on_selection_changed(self): + """Handle selection change.""" + items = self.results_tree.selectedItems() + if items: + self.selected_implant = items[0].data(0, Qt.ItemDataRole.UserRole) + self._update_preview(self.selected_implant) + self.ok_button.setEnabled(True) + else: + self.selected_implant = None + self.ok_button.setEnabled(False) + + def _update_preview(self, implant: NexusMindforceImplant): + """Update preview panel.""" + self.preview_name.setText(implant.name) + self.preview_type.setText(f"{implant.implant_type.title()} ({implant.chip_type})") + self.preview_decay.setText(f"{implant.decay:.4f} PEC per use") + + # Color by type + if implant.implant_type == "healing": + self.preview_type.setStyleSheet("color: #4caf50;") + elif implant.implant_type == "damage": + self.preview_type.setStyleSheet("color: #f44336;") + else: + self.preview_type.setStyleSheet("color: #4a90d9;") + + def _on_double_click(self, item: QTreeWidgetItem, column: int): + """Handle double click.""" + if item.data(0, Qt.ItemDataRole.UserRole): + self._on_accept() + + def _on_accept(self): + """Handle OK button.""" + if self.selected_implant: + self.implant_selected.emit(self.selected_implant) + self.accept() + + +# Main entry for testing +if __name__ == "__main__": + import sys + import logging + from PyQt6.QtWidgets import QApplication + + logging.basicConfig(level=logging.INFO) + + app = QApplication(sys.argv) + app.setStyle('Fusion') + + dialog = MindforceImplantSelectorDialog() + + # Connect signal for testing + dialog.implant_selected.connect(lambda i: print(f"Selected implant: {i.name}")) + + if dialog.exec() == QDialog.DialogCode.Accepted: + print("Implant selected!") + + sys.exit(0)