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()