""" Weapon Attachment Selector for Lemontropia Suite Browse and search weapon attachments (scopes, sights, amplifiers, absorbers) """ from decimal import Decimal from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, QProgressBar, QGroupBox, QFormLayout, QComboBox, QTabWidget, QWidget ) 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, NexusAttachment import logging logger = logging.getLogger(__name__) class AttachmentLoaderThread(QThread): """Background thread for loading attachments from API.""" attachments_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def run(self): try: api = get_nexus_api() # Fetch all attachment types separately all_attachments = [] # Get amplifiers try: amps = api.get_all_amplifiers() all_attachments.extend(amps) except Exception as e: logger.warning(f"Could not load amplifiers: {e}") # Get scopes/sights (both come from same endpoint) try: scopes = api.get_all_scopes() all_attachments.extend(scopes) except Exception as e: logger.warning(f"Could not load scopes: {e}") # Get absorbers try: absorbers = api.get_all_absorbers() all_attachments.extend(absorbers) except Exception as e: logger.warning(f"Could not load absorbers: {e}") self.attachments_loaded.emit(all_attachments) except Exception as e: self.error_occurred.emit(str(e)) class AttachmentSelectorDialog(QDialog): """Dialog for selecting weapon attachments from Entropia Nexus API.""" attachment_selected = pyqtSignal(NexusAttachment) def __init__(self, parent=None, attachment_type: str = ""): super().__init__(parent) self.preferred_type = attachment_type.lower() type_names = { "amplifier": "Amplifier", "scope": "Scope", "sight": "Sight", "absorber": "Absorber" } title_type = type_names.get(self.preferred_type, "Attachment") self.setWindowTitle(f"Select {title_type} - Entropia Nexus") self.setMinimumSize(900, 600) self.all_attachments: List[NexusAttachment] = [] self.selected_attachment: Optional[NexusAttachment] = None self._setup_ui() self._load_data() def _setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) # Status self.status_label = QLabel("Loading attachments from Entropia Nexus...") layout.addWidget(self.status_label) self.progress = QProgressBar() self.progress.setRange(0, 0) layout.addWidget(self.progress) # Type filter tabs self.tabs = QTabWidget() # Create tabs for each attachment type self.tab_all = self._create_attachment_tab("All Attachments") self.tab_amps = self._create_attachment_tab("Amplifiers") self.tab_scopes = self._create_attachment_tab("Scopes") self.tab_sights = self._create_attachment_tab("Sights") self.tab_absorbers = self._create_attachment_tab("Absorbers") self.tabs.addTab(self.tab_all["widget"], "All") self.tabs.addTab(self.tab_amps["widget"], "⚡ Amplifiers") self.tabs.addTab(self.tab_scopes["widget"], "🔭 Scopes") self.tabs.addTab(self.tab_sights["widget"], "🎯 Sights") self.tabs.addTab(self.tab_absorbers["widget"], "🛡️ Absorbers") # Set preferred tab if self.preferred_type == "amplifier": self.tabs.setCurrentIndex(1) elif self.preferred_type == "scope": self.tabs.setCurrentIndex(2) elif self.preferred_type == "sight": self.tabs.setCurrentIndex(3) elif self.preferred_type == "absorber": self.tabs.setCurrentIndex(4) self.tabs.currentChanged.connect(self._on_tab_changed) layout.addWidget(self.tabs) # Preview panel self.preview_group = QGroupBox("Attachment Preview") preview_layout = QFormLayout(self.preview_group) self.preview_name = QLabel("-") self.preview_type = QLabel("-") self.preview_damage = QLabel("-") self.preview_range = QLabel("-") self.preview_zoom = QLabel("-") self.preview_absorption = QLabel("-") self.preview_decay = QLabel("-") self.preview_efficiency = QLabel("-") preview_layout.addRow("Name:", self.preview_name) preview_layout.addRow("Type:", self.preview_type) preview_layout.addRow("Damage Bonus:", self.preview_damage) preview_layout.addRow("Range/Acc Bonus:", self.preview_range) preview_layout.addRow("Zoom:", self.preview_zoom) preview_layout.addRow("Absorption:", self.preview_absorption) preview_layout.addRow("Decay:", self.preview_decay) preview_layout.addRow("Efficiency:", self.preview_efficiency) 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 _create_attachment_tab(self, title: str) -> dict: """Create a tab with search and tree for attachments.""" widget = QWidget() layout = QVBoxLayout(widget) # Search search_layout = QHBoxLayout() search_layout.addWidget(QLabel("Search:")) search_input = QLineEdit() search_input.setPlaceholderText(f"Search {title.lower()}...") search_layout.addWidget(search_input) clear_btn = QPushButton("Clear") search_layout.addWidget(clear_btn) layout.addLayout(search_layout) # Tree - different columns for different attachment types tree = QTreeWidget() tree.setHeaderLabels(["Name", "Type", "Dmg+", "Rng/Acc", "Zoom", "Decay", "Eff%"]) header = tree.header() header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) tree.itemSelectionChanged.connect(self._on_selection_changed) tree.itemDoubleClicked.connect(self._on_double_click) layout.addWidget(tree) return { "widget": widget, "search": search_input, "clear": clear_btn, "tree": tree } def _load_data(self): """Load attachments in background thread.""" self.loader = AttachmentLoaderThread() self.loader.attachments_loaded.connect(self._on_attachments_loaded) self.loader.error_occurred.connect(self._on_load_error) self.loader.start() def _on_attachments_loaded(self, attachments: List[NexusAttachment]): """Handle loaded attachments.""" self.all_attachments = attachments self.status_label.setText(f"Loaded {len(attachments)} attachments from Entropia Nexus") self.progress.setRange(0, 100) self.progress.setValue(100) # Populate all tabs self._populate_tab(self.tab_all, self.all_attachments) self._populate_tab(self.tab_amps, [a for a in attachments if a.attachment_type == "amplifier"]) self._populate_tab(self.tab_scopes, [a for a in attachments if a.attachment_type == "scope"]) self._populate_tab(self.tab_sights, [a for a in attachments if a.attachment_type == "sight"]) self._populate_tab(self.tab_absorbers, [a for a in attachments if a.attachment_type == "absorber"]) # Connect search signals for tab in [self.tab_all, self.tab_amps, self.tab_scopes, self.tab_sights, self.tab_absorbers]: tab["search"].textChanged.connect(lambda text, t=tab: self._on_search_changed(t, text)) tab["clear"].clicked.connect(lambda t=tab: t["search"].clear()) def _on_load_error(self, error: str): """Handle load error.""" self.status_label.setText(f"Error loading attachments: {error}") self.progress.setRange(0, 100) self.progress.setValue(0) def _populate_tab(self, tab: dict, attachments: List[NexusAttachment]): """Populate a tab's tree with attachments.""" tree = tab["tree"] tree.clear() # Sort by damage bonus (amplifiers) or range (scopes) attachments = sorted(attachments, key=lambda a: a.damage_bonus + a.range_bonus, reverse=True) for att in attachments: item = QTreeWidgetItem() item.setText(0, att.name) item.setText(1, att.attachment_type.title()) # Column 2: Damage bonus (for amplifiers) dmg_text = f"+{att.damage_bonus}" if att.damage_bonus > 0 else "-" item.setText(2, dmg_text) # Column 3: Range/Accuracy bonus (for scopes) if att.attachment_type == 'scope' and att.range_bonus > 0: rng_text = f"+{att.range_bonus:.1f}" elif att.attachment_type == 'absorber' and att.absorption > 0: rng_text = f"{att.absorption * 100:.0f}%" else: rng_text = "-" item.setText(3, rng_text) # Column 4: Zoom (for scopes) if att.attachment_type == 'scope' and att.zoom > 0: zoom_text = f"{att.zoom}x" else: zoom_text = "-" item.setText(4, zoom_text) # Column 5: Decay (in PEC) or Absorption if att.attachment_type == 'absorber' and att.absorption > 0: decay_text = f"{att.absorption * 100:.0f}%" else: decay_text = f"{att.decay:.3f}" item.setText(5, decay_text) # Column 6: Efficiency % if att.efficiency_bonus > 0: eff_text = f"{att.efficiency_bonus:.0f}%" else: eff_text = "-" item.setText(6, eff_text) # Color code by type if att.attachment_type == "amplifier": item.setForeground(0, QColor("#ff9800")) # Orange if att.damage_bonus > 0: item.setForeground(2, QColor("#7FFF7F")) elif att.attachment_type == "scope": item.setForeground(0, QColor("#2196f3")) # Blue if att.zoom > 0: item.setForeground(4, QColor("#00FFFF")) elif att.attachment_type == "sight": item.setForeground(0, QColor("#4caf50")) # Green elif att.attachment_type == "absorber": item.setForeground(0, QColor("#9c27b0")) # Purple item.setData(0, Qt.ItemDataRole.UserRole, att) tree.addTopLevelItem(item) item.setData(0, Qt.ItemDataRole.UserRole, att) tree.addTopLevelItem(item) def _on_search_changed(self, tab: dict, text: str): """Handle search text change in a tab.""" tree = tab["tree"] for i in range(tree.topLevelItemCount()): item = tree.topLevelItem(i) attachment = item.data(0, Qt.ItemDataRole.UserRole) if text.lower() in attachment.name.lower(): item.setHidden(False) else: item.setHidden(True) def _on_tab_changed(self, index: int): """Handle tab change.""" # Clear selection when changing tabs self.selected_attachment = None self.ok_button.setEnabled(False) self._clear_preview() def _on_selection_changed(self): """Handle selection change in current tab.""" current_tab = self._get_current_tab() if current_tab: items = current_tab["tree"].selectedItems() if items: self.selected_attachment = items[0].data(0, Qt.ItemDataRole.UserRole) self.ok_button.setEnabled(True) self._update_preview(self.selected_attachment) else: self.selected_attachment = None self.ok_button.setEnabled(False) self._clear_preview() def _get_current_tab(self) -> Optional[dict]: """Get the currently active tab data.""" index = self.tabs.currentIndex() tabs = [self.tab_all, self.tab_amps, self.tab_scopes, self.tab_sights, self.tab_absorbers] if 0 <= index < len(tabs): return tabs[index] return None def _update_preview(self, attachment: NexusAttachment): """Update preview panel.""" self.preview_name.setText(attachment.name) self.preview_type.setText(attachment.attachment_type.title()) # Damage bonus (amplifiers) if attachment.damage_bonus > 0: self.preview_damage.setText(f"+{attachment.damage_bonus}") self.preview_damage.setStyleSheet("color: #7FFF7F;") else: self.preview_damage.setText("-") self.preview_damage.setStyleSheet("") # Range/Accuracy bonus (scopes) if attachment.range_bonus > 0: self.preview_range.setText(f"+{attachment.range_bonus:.1f}") self.preview_range.setStyleSheet("color: #7FFF7F;") else: self.preview_range.setText("-") self.preview_range.setStyleSheet("") # Zoom (scopes) if attachment.zoom > 0: self.preview_zoom.setText(f"{attachment.zoom}x") self.preview_zoom.setStyleSheet("color: #00FFFF;") else: self.preview_zoom.setText("-") self.preview_zoom.setStyleSheet("") # Absorption (absorbers) if attachment.absorption > 0: self.preview_absorption.setText(f"{attachment.absorption * 100:.1f}%") self.preview_absorption.setStyleSheet("color: #9c27b0;") else: self.preview_absorption.setText("-") self.preview_absorption.setStyleSheet("") # Decay or Absorption if attachment.attachment_type == 'absorber' and attachment.absorption > 0: self.preview_decay.setText(f"{attachment.absorption * 100:.1f}% absorption") self.preview_decay.setStyleSheet("color: #9c27b0;") else: self.preview_decay.setText(f"{attachment.decay:.4f} PEC") self.preview_decay.setStyleSheet("") # Efficiency if attachment.efficiency_bonus > 0: self.preview_efficiency.setText(f"{attachment.efficiency_bonus:.0f}%") else: self.preview_efficiency.setText("-") def _clear_preview(self): """Clear preview panel.""" self.preview_name.setText("-") self.preview_type.setText("-") self.preview_damage.setText("-") self.preview_range.setText("-") self.preview_zoom.setText("-") self.preview_absorption.setText("-") self.preview_decay.setText("-") self.preview_efficiency.setText("-") # Reset stylesheets for label in [self.preview_damage, self.preview_range, self.preview_zoom, self.preview_absorption]: label.setStyleSheet("") def _on_double_click(self, item, column): """Handle double click.""" self._on_accept() def _on_accept(self): """Handle OK button.""" if self.selected_attachment: self.attachment_selected.emit(self.selected_attachment) self.accept()