From 1c0d684c4c2dd07aa48834b2562fcc1a439aae3b Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 11:50:45 +0000 Subject: [PATCH] feat(selectors): add plate and attachment selectors - NEW: ui/plate_selector.py - Searchable plate browser - Filter by protection type (Impact, Cut, etc.) - Filter by minimum protection - Highlights plates matching mob damage type - NEW: ui/attachment_selector.py - Tabbed attachment browser - Tabs: All, Amplifiers, Scopes, Sights, Absorbers - Search within each category - Color-coded by attachment type --- ui/attachment_selector.py | 865 +++++++++++--------------------------- ui/plate_selector.py | 262 ++++++++++++ 2 files changed, 499 insertions(+), 628 deletions(-) create mode 100644 ui/plate_selector.py diff --git a/ui/attachment_selector.py b/ui/attachment_selector.py index afb022a..f1820d7 100644 --- a/ui/attachment_selector.py +++ b/ui/attachment_selector.py @@ -1,678 +1,287 @@ """ -Attachment Selector Dialog for Lemontropia Suite -UI for selecting and managing gear attachments. +Weapon Attachment Selector for Lemontropia Suite +Browse and search weapon attachments (scopes, sights, amplifiers, absorbers) """ -from PyQt6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QListWidget, QListWidgetItem, QDialogButtonBox, QFormLayout, - QTabWidget, QWidget -) -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtGui import QFont from decimal import Decimal -from typing import Optional, List, Dict - -from core.attachments import ( - Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber, - ArmorPlating, FinderAmplifier, Enhancer, MindforceImplant, - get_mock_attachments +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 -# ============================================================================ -# Data Class for Attachment Configuration -# ============================================================================ +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() + attachments = api.get_all_attachments() + self.attachments_loaded.emit(attachments) + except Exception as e: + self.error_occurred.emit(str(e)) -class AttachmentConfig: - """Configuration for an equipped attachment.""" - - def __init__( - self, - name: str, - item_id: str, - attachment_type: str, - decay_pec: Decimal, - damage_bonus: Decimal = Decimal("0"), - range_bonus: Decimal = Decimal("0"), - efficiency_bonus: Decimal = Decimal("0"), - protection_bonus: Optional[Dict[str, Decimal]] = None, - extra_data: Optional[Dict] = None - ): - self.name = name - self.item_id = item_id - self.attachment_type = attachment_type - self.decay_pec = decay_pec - self.damage_bonus = damage_bonus - self.range_bonus = range_bonus - self.efficiency_bonus = efficiency_bonus - self.protection_bonus = protection_bonus or {} - self.extra_data = extra_data or {} - - def to_dict(self) -> dict: - """Convert to dictionary for JSON serialization.""" - return { - 'name': self.name, - 'item_id': self.item_id, - 'attachment_type': self.attachment_type, - 'decay_pec': str(self.decay_pec), - 'damage_bonus': str(self.damage_bonus), - 'range_bonus': str(self.range_bonus), - 'efficiency_bonus': str(self.efficiency_bonus), - 'protection_bonus': {k: str(v) for k, v in self.protection_bonus.items()}, - 'extra_data': self.extra_data, - } - - @classmethod - def from_dict(cls, data: dict) -> 'AttachmentConfig': - """Create from dictionary.""" - return cls( - name=data['name'], - item_id=data['item_id'], - attachment_type=data['attachment_type'], - decay_pec=Decimal(data['decay_pec']), - damage_bonus=Decimal(data.get('damage_bonus', '0')), - range_bonus=Decimal(data.get('range_bonus', '0')), - efficiency_bonus=Decimal(data.get('efficiency_bonus', '0')), - protection_bonus={k: Decimal(v) for k, v in data.get('protection_bonus', {}).items()}, - extra_data=data.get('extra_data', {}), - ) - - def get_cost_per_hour(self, uses_per_hour: int = 3600) -> Decimal: - """Calculate cost per hour in PED.""" - return (self.decay_pec * Decimal(uses_per_hour)) / Decimal("100") - - def get_tooltip_text(self) -> str: - """Get tooltip text describing the attachment.""" - lines = [f"Decay: {self.decay_pec} PEC/shot"] - - if self.damage_bonus > 0: - lines.append(f"Damage: +{self.damage_bonus}") - if self.range_bonus > 0: - lines.append(f"Range: +{self.range_bonus}m") - if self.efficiency_bonus > 0: - lines.append(f"Efficiency: +{self.efficiency_bonus}%") - - if self.protection_bonus: - prots = [f"{k}: +{v}" for k, v in self.protection_bonus.items() if v > 0] - if prots: - lines.append("Protection: " + ", ".join(prots)) - - return "\n".join(lines) - - -# ============================================================================ -# Attachment Selector Dialog -# ============================================================================ class AttachmentSelectorDialog(QDialog): - """ - Dialog for selecting attachments. + """Dialog for selecting weapon attachments from Entropia Nexus API.""" - Supports all attachment types: - - amplifier: Weapon amplifiers (damage boost) - - scope: Weapon scopes (range boost) - - absorber: Weapon absorbers (damage reduction) - - plating: Armor plating (protection boost) - - finder_amp: Mining finder amplifiers - - enhancer: Gear enhancers - - implant: Mindforce implants - """ + attachment_selected = pyqtSignal(NexusAttachment) - attachment_selected = pyqtSignal(object) # AttachmentConfig or None - - # Friendly names for attachment types - TYPE_NAMES = { - 'amplifier': 'Weapon Amplifier', - 'scope': 'Weapon Scope', - 'absorber': 'Weapon Absorber', - 'plating': 'Armor Plating', - 'finder_amp': 'Finder Amplifier', - 'enhancer': 'Enhancer', - 'implant': 'Mindforce Implant', - } - - # Icons for attachment types - TYPE_ICONS = { - 'amplifier': '⚡', - 'scope': '🔭', - 'absorber': '🛡️', - 'plating': '🔩', - 'finder_amp': '⛏️', - 'enhancer': '✨', - 'implant': '🧠', - } - - def __init__(self, attachment_type: str, parent=None, allow_none: bool = True): - """ - Initialize attachment selector. - - Args: - attachment_type: Type of attachment to select - parent: Parent widget - allow_none: Whether to allow selecting "None" - """ + def __init__(self, parent=None, attachment_type: str = ""): super().__init__(parent) - self.attachment_type = attachment_type - self.selected_attachment = None - self.allow_none = allow_none + self.preferred_type = attachment_type.lower() - type_name = self.TYPE_NAMES.get(attachment_type, attachment_type.title()) - self.setWindowTitle(f"Select {type_name}") - self.setMinimumSize(500, 450) + 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_attachments() + self._load_data() def _setup_ui(self): - """Setup the UI.""" layout = QVBoxLayout(self) layout.setSpacing(10) - # Header - type_name = self.TYPE_NAMES.get(self.attachment_type, self.attachment_type.title()) - type_icon = self.TYPE_ICONS.get(self.attachment_type, '📎') + # Status + self.status_label = QLabel("Loading attachments from Entropia Nexus...") + layout.addWidget(self.status_label) - header = QLabel(f"{type_icon} Select {type_name}") - header.setFont(QFont("Arial", 14, QFont.Weight.Bold)) - header.setStyleSheet("color: #4a90d9; padding: 5px;") - layout.addWidget(header) + self.progress = QProgressBar() + self.progress.setRange(0, 0) + layout.addWidget(self.progress) - # Description - desc = self._get_type_description() - if desc: - desc_label = QLabel(desc) - desc_label.setStyleSheet("color: #888888; padding-bottom: 10px;") - desc_label.setWordWrap(True) - layout.addWidget(desc_label) + # Type filter tabs + self.tabs = QTabWidget() - # Attachment list - self.list_widget = QListWidget() - self.list_widget.setAlternatingRowColors(True) - self.list_widget.setStyleSheet(""" - QListWidget { - background-color: #2d2d2d; - color: #e0e0e0; - border: 1px solid #3d3d3d; - border-radius: 4px; - } - QListWidget::item:selected { - background-color: #4a90d9; - } - QListWidget::item:hover { - background-color: #3d3d3d; - } - """) - self.list_widget.itemSelectionChanged.connect(self._on_selection_changed) - self.list_widget.itemDoubleClicked.connect(self._on_double_click) - layout.addWidget(self.list_widget) + # 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") - # Stats preview - self.preview_group = QWidget() + 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) - preview_layout.setContentsMargins(10, 10, 10, 10) - self.preview_group.setStyleSheet(""" - QWidget { - background-color: #2d2d2d; - border-radius: 4px; - } - QLabel { - color: #e0e0e0; - } - """) - - self.preview_name = QLabel("No attachment selected") - self.preview_name.setStyleSheet("font-weight: bold; color: #4caf50;") - self.preview_stats = QLabel("") - self.preview_stats.setStyleSheet("color: #888888;") - - preview_layout.addRow(self.preview_name) - preview_layout.addRow(self.preview_stats) + self.preview_name = QLabel("-") + self.preview_type = QLabel("-") + self.preview_damage = QLabel("-") + self.preview_range = 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 Bonus:", self.preview_range) + preview_layout.addRow("Decay:", self.preview_decay) + preview_layout.addRow("Efficiency:", self.preview_efficiency) layout.addWidget(self.preview_group) # Buttons - button_layout = QHBoxLayout() - - if self.allow_none: - self.none_btn = QPushButton("❌ Remove Attachment") - self.none_btn.setStyleSheet(""" - QPushButton { - background-color: #7d2e2e; - color: #e0e0e0; - border: 1px solid #f44336; - border-radius: 4px; - padding: 8px 16px; - } - QPushButton:hover { - background-color: #f44336; - } - """) - self.none_btn.clicked.connect(self._on_none) - button_layout.addWidget(self.none_btn) - - button_layout.addStretch() - - self.ok_btn = QPushButton("✓ Select") - self.ok_btn.setEnabled(False) - self.ok_btn.setStyleSheet(""" - QPushButton { - background-color: #2e7d32; - color: #e0e0e0; - border: 1px solid #4caf50; - border-radius: 4px; - padding: 8px 24px; - } - QPushButton:hover { - background-color: #4caf50; - } - QPushButton:disabled { - background-color: #3d3d3d; - border-color: #4d4d4d; - } - """) - self.ok_btn.clicked.connect(self._on_accept) - - cancel_btn = QPushButton("Cancel") - cancel_btn.clicked.connect(self.reject) - - button_layout.addWidget(self.ok_btn) - button_layout.addWidget(cancel_btn) - layout.addLayout(button_layout) + 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 _get_type_description(self) -> str: - """Get description for attachment type.""" - descriptions = { - 'amplifier': 'Amplifiers increase weapon damage at the cost of additional decay and ammo.', - 'scope': 'Scopes increase weapon range for better accuracy at distance.', - 'absorber': 'Absorbers reduce incoming damage when attached to weapons.', - 'plating': 'Armor plating adds protection values to your armor set.', - 'finder_amp': 'Finder amplifiers increase mining depth and radius.', - 'enhancer': 'Enhancers add special effects to gear.', - 'implant': 'Mindforce implants boost mindforce chip performance.', + 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 + tree = QTreeWidget() + tree.setHeaderLabels(["Name", "Type", "Dmg+", "Rng+", "Decay", "Efficiency"]) + 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 } - return descriptions.get(self.attachment_type, "") - def _load_attachments(self): - """Load attachments from data source.""" - attachments = get_mock_attachments(self.attachment_type) - self.attachments = attachments + 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) - self.list_widget.clear() + # 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: - icon = self.TYPE_ICONS.get(self.attachment_type, '📎') - item = QListWidgetItem(f"{icon} {att.name}") - item.setData(Qt.ItemDataRole.UserRole, att) + item = QTreeWidgetItem() + item.setText(0, att.name) + item.setText(1, att.attachment_type.title()) + item.setText(2, f"+{att.damage_bonus}") + item.setText(3, f"+{att.range_bonus}") + item.setText(4, f"{att.decay:.2f}") + item.setText(5, f"{att.efficiency_bonus:.1f}%") - # Build tooltip - tooltip_lines = [f"Decay: {att.decay_pec} PEC/shot"] + # Color code by type + if att.attachment_type == "amplifier": + item.setForeground(0, QColor("#ff9800")) # Orange + elif att.attachment_type == "scope": + item.setForeground(0, QColor("#2196f3")) # Blue + elif att.attachment_type == "sight": + item.setForeground(0, QColor("#4caf50")) # Green + elif att.attachment_type == "absorber": + item.setForeground(0, QColor("#9c27b0")) # Purple - if isinstance(att, WeaponAmplifier): - tooltip_lines.append(f"Damage: +{att.damage_increase}") - tooltip_lines.append(f"Ammo: +{att.ammo_increase}") - elif isinstance(att, WeaponScope): - tooltip_lines.append(f"Range: +{att.range_increase}m") - if att.accuracy_bonus > 0: - tooltip_lines.append(f"Accuracy: +{att.accuracy_bonus}") - elif isinstance(att, WeaponAbsorber): - tooltip_lines.append(f"Damage Reduction: {att.damage_reduction}") - elif isinstance(att, ArmorPlating): - tooltip_lines.append(f"Total Protection: +{att.get_total_protection()}") - # Add individual protections - prots = [] - if att.protection_impact > 0: - prots.append(f"Impact +{att.protection_impact}") - if att.protection_cut > 0: - prots.append(f"Cut +{att.protection_cut}") - if att.protection_stab > 0: - prots.append(f"Stab +{att.protection_stab}") - if att.protection_burn > 0: - prots.append(f"Burn +{att.protection_burn}") - if prots: - tooltip_lines.append(", ".join(prots)) - elif isinstance(att, FinderAmplifier): - tooltip_lines.append(f"Depth: +{att.depth_increase}m") - tooltip_lines.append(f"Radius: +{att.radius_increase}m") - elif isinstance(att, Enhancer): - tooltip_lines.append(f"Tier: {att.tier}") - tooltip_lines.append(f"Effect: {att.effect_name} +{att.effect_value}") - elif isinstance(att, MindforceImplant): - tooltip_lines.append(f"Mindforce Bonus: +{att.mindforce_bonus}") - - item.setToolTip("\n".join(tooltip_lines)) - self.list_widget.addItem(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.""" - selected = self.list_widget.selectedItems() - if selected: - attachment = selected[0].data(Qt.ItemDataRole.UserRole) - self.selected_attachment = attachment - self.ok_btn.setEnabled(True) - self._update_preview(attachment) - else: - self.selected_attachment = None - self.ok_btn.setEnabled(False) - self.preview_name.setText("No attachment selected") - self.preview_stats.setText("") + """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 _update_preview(self, attachment): + 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) - - stats_lines = [] - stats_lines.append(f"Decay: {attachment.decay_pec} PEC/shot") - - if isinstance(attachment, WeaponAmplifier): - stats_lines.append(f"Damage Increase: +{attachment.damage_increase}") - stats_lines.append(f"Ammo Increase: +{attachment.ammo_increase}") - - # Calculate DPP impact - if attachment.damage_increase > 0: - # Rough DPP calculation for the amp - amp_cost = attachment.decay_pec + (Decimal(attachment.ammo_increase) * Decimal("0.01")) - amp_dpp = attachment.damage_increase / amp_cost if amp_cost > 0 else Decimal("0") - stats_lines.append(f"Amp DPP: {amp_dpp:.2f}") - - elif isinstance(attachment, WeaponScope): - stats_lines.append(f"Range Increase: +{attachment.range_increase}m") - if attachment.accuracy_bonus > 0: - stats_lines.append(f"Accuracy Bonus: +{attachment.accuracy_bonus}") - - elif isinstance(attachment, WeaponAbsorber): - stats_lines.append(f"Damage Reduction: -{attachment.damage_reduction}") - - elif isinstance(attachment, ArmorPlating): - total = attachment.get_total_protection() - stats_lines.append(f"Total Protection: +{total}") - - # Individual protections - prots = [] - if attachment.protection_impact > 0: - prots.append(f"Impact +{attachment.protection_impact}") - if attachment.protection_cut > 0: - prots.append(f"Cut +{attachment.protection_cut}") - if attachment.protection_stab > 0: - prots.append(f"Stab +{attachment.protection_stab}") - if attachment.protection_penetration > 0: - prots.append(f"Pen +{attachment.protection_penetration}") - if attachment.protection_burn > 0: - prots.append(f"Burn +{attachment.protection_burn}") - if attachment.protection_cold > 0: - prots.append(f"Cold +{attachment.protection_cold}") - if attachment.protection_acid > 0: - prots.append(f"Acid +{attachment.protection_acid}") - if attachment.protection_electric > 0: - prots.append(f"Elec +{attachment.protection_electric}") - - if prots: - stats_lines.append(f"Protection: {', '.join(prots)}") - - elif isinstance(attachment, FinderAmplifier): - stats_lines.append(f"Depth: +{attachment.depth_increase}m") - stats_lines.append(f"Radius: +{attachment.radius_increase}m") - - elif isinstance(attachment, Enhancer): - stats_lines.append(f"Tier: {attachment.tier}") - stats_lines.append(f"Effect: {attachment.effect_name} +{attachment.effect_value}") - - elif isinstance(attachment, MindforceImplant): - stats_lines.append(f"Mindforce Bonus: +{attachment.mindforce_bonus}") - - # Calculate cost per hour (assuming 3600 uses/hour) - cost_per_hour = (attachment.decay_pec * Decimal("3600")) / Decimal("100") - stats_lines.append(f"Cost/Hour: {cost_per_hour:.2f} PED") - - self.preview_stats.setText("
".join(stats_lines)) + self.preview_type.setText(attachment.attachment_type.title()) + self.preview_damage.setText(f"+{attachment.damage_bonus}") + self.preview_range.setText(f"+{attachment.range_bonus}") + self.preview_decay.setText(f"{attachment.decay:.2f} PEC") + self.preview_efficiency.setText(f"{attachment.efficiency_bonus:.1f}%") - def _on_double_click(self, item): + 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_decay.setText("-") + self.preview_efficiency.setText("-") + + def _on_double_click(self, item, column): """Handle double click.""" self._on_accept() def _on_accept(self): """Handle OK button.""" if self.selected_attachment: - att = self.selected_attachment - - # Build protection bonus dict for armor plating - protection_bonus = {} - if isinstance(att, ArmorPlating): - protection_bonus = { - 'stab': att.protection_stab, - 'cut': att.protection_cut, - 'impact': att.protection_impact, - 'penetration': att.protection_penetration, - 'shrapnel': att.protection_shrapnel, - 'burn': att.protection_burn, - 'cold': att.protection_cold, - 'acid': att.protection_acid, - 'electric': att.protection_electric, - } - - # Build extra data based on type - extra_data = {} - if isinstance(att, WeaponAmplifier): - extra_data['ammo_increase'] = att.ammo_increase - elif isinstance(att, Enhancer): - extra_data['tier'] = att.tier - extra_data['effect_name'] = att.effect_name - extra_data['effect_value'] = float(att.effect_value) - - config = AttachmentConfig( - name=att.name, - item_id=att.item_id, - attachment_type=att.attachment_type, - decay_pec=att.decay_pec, - damage_bonus=getattr(att, 'damage_increase', Decimal("0")), - range_bonus=getattr(att, 'range_increase', Decimal("0")), - efficiency_bonus=Decimal("0"), - protection_bonus=protection_bonus, - extra_data=extra_data, - ) - - self.attachment_selected.emit(config) + self.attachment_selected.emit(self.selected_attachment) self.accept() - - def _on_none(self): - """Remove attachment.""" - self.attachment_selected.emit(None) - self.accept() - - @staticmethod - def select_attachment(attachment_type: str, parent=None, allow_none: bool = True) -> Optional[AttachmentConfig]: - """ - Static method to open selector and return selected attachment. - - Args: - attachment_type: Type of attachment to select - parent: Parent widget - allow_none: Whether to allow selecting "None" - - Returns: - AttachmentConfig or None if cancelled or "None" selected - """ - dialog = AttachmentSelectorDialog(attachment_type, parent, allow_none) - result = None - - def on_selected(att): - nonlocal result - result = att - - dialog.attachment_selected.connect(on_selected) - - if dialog.exec() == QDialog.DialogCode.Accepted: - return result - return None - - -# ============================================================================ -# Multi-Attachment Manager -# ============================================================================ - -class AttachmentManagerWidget(QWidget): - """ - Widget for managing multiple attachments on a piece of gear. - Shows currently equipped attachments and allows adding/removing. - """ - - attachment_changed = pyqtSignal(str, object) # slot_name, AttachmentConfig or None - - def __init__(self, gear_type: str = 'weapon', parent=None): - super().__init__(parent) - self.gear_type = gear_type - self.attachments: Dict[str, Optional[AttachmentConfig]] = {} - - self._setup_ui() - - def _setup_ui(self): - """Setup the UI.""" - from core.attachments import get_compatible_attachments - - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - # Get compatible attachment types - compatible_types = get_compatible_attachments(self.gear_type) - - for att_type in compatible_types: - type_name = AttachmentSelectorDialog.TYPE_NAMES.get(att_type, att_type.title()) - type_icon = AttachmentSelectorDialog.TYPE_ICONS.get(att_type, '📎') - - row = QWidget() - row_layout = QHBoxLayout(row) - row_layout.setContentsMargins(0, 0, 0, 0) - - label = QLabel(f"{type_icon} {type_name}:") - label.setFixedWidth(120) - row_layout.addWidget(label) - - status_label = QLabel("None") - status_label.setStyleSheet("color: #888888;") - row_layout.addWidget(status_label, stretch=1) - - add_btn = QPushButton("+") - add_btn.setFixedSize(30, 24) - add_btn.setStyleSheet(""" - QPushButton { - background-color: #2e7d32; - color: white; - border-radius: 4px; - } - QPushButton:hover { - background-color: #4caf50; - } - """) - add_btn.clicked.connect(lambda checked, t=att_type: self._on_add(t)) - row_layout.addWidget(add_btn) - - remove_btn = QPushButton("✕") - remove_btn.setFixedSize(30, 24) - remove_btn.setEnabled(False) - remove_btn.setStyleSheet(""" - QPushButton { - background-color: #7d2e2e; - color: white; - border-radius: 4px; - } - QPushButton:hover { - background-color: #f44336; - } - QPushButton:disabled { - background-color: #3d3d3d; - } - """) - remove_btn.clicked.connect(lambda checked, t=att_type: self._on_remove(t)) - row_layout.addWidget(remove_btn) - - layout.addWidget(row) - - # Store references - self.attachments[att_type] = None - setattr(self, f"{att_type}_status", status_label) - setattr(self, f"{att_type}_add_btn", add_btn) - setattr(self, f"{att_type}_remove_btn", remove_btn) - - def _on_add(self, attachment_type: str): - """Open selector for attachment type.""" - dialog = AttachmentSelectorDialog(attachment_type, self, allow_none=False) - - def on_selected(att): - if att: - self.attachments[attachment_type] = att - self._update_display(attachment_type, att) - self.attachment_changed.emit(attachment_type, att) - - dialog.attachment_selected.connect(on_selected) - dialog.exec() - - def _on_remove(self, attachment_type: str): - """Remove attachment.""" - self.attachments[attachment_type] = None - self._update_display(attachment_type, None) - self.attachment_changed.emit(attachment_type, None) - - def _update_display(self, attachment_type: str, att: Optional[AttachmentConfig]): - """Update display for attachment type.""" - status_label = getattr(self, f"{attachment_type}_status") - remove_btn = getattr(self, f"{attachment_type}_remove_btn") - - if att: - # Build status text - status_parts = [att.name] - if att.damage_bonus > 0: - status_parts.append(f"(+{att.damage_bonus} dmg)") - elif att.range_bonus > 0: - status_parts.append(f"(+{att.range_bonus}m)") - - status_label.setText(" ".join(status_parts)) - status_label.setStyleSheet("color: #4caf50;") - remove_btn.setEnabled(True) - else: - status_label.setText("None") - status_label.setStyleSheet("color: #888888;") - remove_btn.setEnabled(False) - - def get_attachment(self, attachment_type: str) -> Optional[AttachmentConfig]: - """Get attachment of given type.""" - return self.attachments.get(attachment_type) - - def set_attachment(self, attachment_type: str, att: Optional[AttachmentConfig]): - """Set attachment of given type.""" - if attachment_type in self.attachments: - self.attachments[attachment_type] = att - self._update_display(attachment_type, att) - - def get_all_attachments(self) -> Dict[str, Optional[AttachmentConfig]]: - """Get all attachments.""" - return self.attachments.copy() - - -# ============================================================================ -# Main entry point for testing -# ============================================================================ - -def main(): - """Test the attachment selector.""" - import sys - from PyQt6.QtWidgets import QApplication - - app = QApplication(sys.argv) - app.setStyle('Fusion') - - # Test single selector - print("Testing single attachment selector...") - result = AttachmentSelectorDialog.select_attachment('amplifier') - - if result: - print(f"Selected: {result.name}") - print(f" Decay: {result.decay_pec} PEC") - print(f" Damage Bonus: {result.damage_bonus}") - else: - print("No attachment selected") - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/ui/plate_selector.py b/ui/plate_selector.py new file mode 100644 index 0000000..9deef4c --- /dev/null +++ b/ui/plate_selector.py @@ -0,0 +1,262 @@ +""" +Plate Selector for Lemontropia Suite +Browse and search armor plates 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, NexusPlate + + +class PlateLoaderThread(QThread): + """Background thread for loading plates from API.""" + plates_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = get_nexus_api() + plates = api.get_all_plates() + self.plates_loaded.emit(plates) + except Exception as e: + self.error_occurred.emit(str(e)) + + +class PlateSelectorDialog(QDialog): + """Dialog for selecting armor plates from Entropia Nexus API.""" + + plate_selected = pyqtSignal(NexusPlate) + + def __init__(self, parent=None, damage_type: str = ""): + super().__init__(parent) + self.setWindowTitle("Select Armor Plate - Entropia Nexus") + self.setMinimumSize(900, 600) + + self.all_plates: List[NexusPlate] = [] + self.selected_plate: Optional[NexusPlate] = None + self.preferred_damage_type = damage_type # For filtering + + self._setup_ui() + self._load_data() + + def _setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(10) + + # Status + self.status_label = QLabel("Loading plates 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("Protection Type:")) + self.type_combo = QComboBox() + self.type_combo.addItems(["All Types", "Impact", "Cut", "Stab", "Burn", "Cold", "Acid", "Electric"]) + if self.preferred_damage_type: + index = self.type_combo.findText(self.preferred_damage_type.capitalize()) + if index >= 0: + self.type_combo.setCurrentIndex(index) + self.type_combo.currentTextChanged.connect(self._apply_filters) + filter_layout.addWidget(self.type_combo) + + filter_layout.addWidget(QLabel("Min Protection:")) + self.min_prot_combo = QComboBox() + self.min_prot_combo.addItems(["Any", "1+", "3+", "5+", "7+", "10+"]) + self.min_prot_combo.currentTextChanged.connect(self._apply_filters) + filter_layout.addWidget(self.min_prot_combo) + + layout.addLayout(filter_layout) + + # Search + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Type to search plates (e.g., '5B', 'Impact')...") + self.search_input.textChanged.connect(self._apply_filters) + search_layout.addWidget(self.search_input) + + self.clear_btn = QPushButton("Clear") + self.clear_btn.clicked.connect(self._clear_search) + search_layout.addWidget(self.clear_btn) + layout.addLayout(search_layout) + + # Results tree + self.results_tree = QTreeWidget() + self.results_tree.setHeaderLabels([ + "Name", "Impact", "Cut", "Stab", "Burn", "Cold", "Acid", "Electric", "Total", "Decay" + ]) + header = self.results_tree.header() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + header.setStretchLastSection(False) + 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("Plate Preview") + preview_layout = QFormLayout(self.preview_group) + self.preview_name = QLabel("-") + self.preview_protection = QLabel("-") + self.preview_decay = QLabel("-") + preview_layout.addRow("Name:", self.preview_name) + preview_layout.addRow("Protection:", self.preview_protection) + preview_layout.addRow("Decay:", 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 plates in background thread.""" + self.loader = PlateLoaderThread() + self.loader.plates_loaded.connect(self._on_plates_loaded) + self.loader.error_occurred.connect(self._on_load_error) + self.loader.start() + + def _on_plates_loaded(self, plates: List[NexusPlate]): + """Handle loaded plates.""" + self.all_plates = plates + self.status_label.setText(f"Loaded {len(plates)} plates from Entropia Nexus") + self.progress.setRange(0, 100) + self.progress.setValue(100) + self._apply_filters() + + def _on_load_error(self, error: str): + """Handle load error.""" + self.status_label.setText(f"Error loading plates: {error}") + self.progress.setRange(0, 100) + self.progress.setValue(0) + + def _apply_filters(self): + """Apply all filters and search.""" + plates = self.all_plates.copy() + + # Type filter + type_filter = self.type_combo.currentText() + if type_filter != "All Types": + type_lower = type_filter.lower() + plates = [p for p in plates if getattr(p, f"protection_{type_lower}", Decimal("0")) > 0] + + # Min protection filter + min_prot = self.min_prot_combo.currentText() + if min_prot != "Any": + min_val = int(min_prot.replace("+", "")) + plates = [p for p in plates if ( + p.protection_impact + p.protection_cut + p.protection_stab + + p.protection_burn + p.protection_cold + p.protection_acid + p.protection_electric + ) >= min_val] + + # Search filter + search_text = self.search_input.text() + if search_text: + query = search_text.lower() + plates = [p for p in plates if query in p.name.lower()] + + self._populate_results(plates) + + # Update status + if search_text: + self.status_label.setText(f"Found {len(plates)} plates matching '{search_text}'") + else: + self.status_label.setText(f"Showing {len(plates)} of {len(self.all_plates)} plates") + + def _populate_results(self, plates: List[NexusPlate]): + """Populate results tree.""" + self.results_tree.clear() + + # Sort by total protection (highest first) + plates = sorted(plates, key=lambda p: ( + p.protection_impact + p.protection_cut + p.protection_stab + + p.protection_burn + p.protection_cold + p.protection_acid + p.protection_electric + ), reverse=True) + + for plate in plates: + item = QTreeWidgetItem() + item.setText(0, plate.name) + item.setText(1, str(plate.protection_impact)) + item.setText(2, str(plate.protection_cut)) + item.setText(3, str(plate.protection_stab)) + item.setText(4, str(plate.protection_burn)) + item.setText(5, str(plate.protection_cold)) + item.setText(6, str(plate.protection_acid)) + item.setText(7, str(plate.protection_electric)) + + total = plate.protection_impact + plate.protection_cut + plate.protection_stab + plate.protection_burn + plate.protection_cold + plate.protection_acid + plate.protection_electric + item.setText(8, str(total)) + item.setText(9, f"{plate.decay:.2f}") + + # Highlight plates matching preferred damage type + if self.preferred_damage_type: + type_lower = self.preferred_damage_type.lower() + if getattr(plate, f"protection_{type_lower}", Decimal("0")) > 0: + item.setBackground(0, QColor("#2d4a3e")) # Dark green + + item.setData(0, Qt.ItemDataRole.UserRole, plate) + self.results_tree.addTopLevelItem(item) + + def _clear_search(self): + """Clear search and filters.""" + self.search_input.clear() + self.type_combo.setCurrentIndex(0) + self.min_prot_combo.setCurrentIndex(0) + self._apply_filters() + + def _on_selection_changed(self): + """Handle selection change.""" + items = self.results_tree.selectedItems() + if items: + self.selected_plate = items[0].data(0, Qt.ItemDataRole.UserRole) + self.ok_button.setEnabled(True) + self._update_preview(self.selected_plate) + else: + self.selected_plate = None + self.ok_button.setEnabled(False) + + def _update_preview(self, plate: NexusPlate): + """Update preview panel.""" + self.preview_name.setText(plate.name) + + prot_parts = [] + if plate.protection_impact > 0: + prot_parts.append(f"Imp:{plate.protection_impact}") + if plate.protection_cut > 0: + prot_parts.append(f"Cut:{plate.protection_cut}") + if plate.protection_stab > 0: + prot_parts.append(f"Stab:{plate.protection_stab}") + if plate.protection_burn > 0: + prot_parts.append(f"Burn:{plate.protection_burn}") + if plate.protection_cold > 0: + prot_parts.append(f"Cold:{plate.protection_cold}") + + self.preview_protection.setText(", ".join(prot_parts) if prot_parts else "None") + self.preview_decay.setText(f"{plate.decay:.2f} PEC") + + def _on_double_click(self, item, column): + """Handle double click.""" + self._on_accept() + + def _on_accept(self): + """Handle OK button.""" + if self.selected_plate: + self.plate_selected.emit(self.selected_plate) + self.accept()