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
This commit is contained in:
LemonNexus 2026-02-09 11:50:45 +00:00
parent 649aa77bc9
commit 1c0d684c4c
2 changed files with 499 additions and 628 deletions

View File

@ -1,678 +1,287 @@
""" """
Attachment Selector Dialog for Lemontropia Suite Weapon Attachment Selector for Lemontropia Suite
UI for selecting and managing gear attachments. 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 decimal import Decimal
from typing import Optional, List, Dict from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
from core.attachments import ( QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox,
Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber, QProgressBar, QGroupBox, QFormLayout, QComboBox, QTabWidget, QWidget
ArmorPlating, FinderAmplifier, Enhancer, MindforceImplant,
get_mock_attachments
) )
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
# ============================================================================ class AttachmentLoaderThread(QThread):
# Data Class for Attachment Configuration """Background thread for loading attachments from API."""
# ============================================================================ attachments_loaded = pyqtSignal(list)
error_occurred = pyqtSignal(str)
class AttachmentConfig: def run(self):
"""Configuration for an equipped attachment.""" 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))
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): class AttachmentSelectorDialog(QDialog):
""" """Dialog for selecting weapon attachments from Entropia Nexus API."""
Dialog for selecting attachments.
Supports all attachment types: attachment_selected = pyqtSignal(NexusAttachment)
- 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(object) # AttachmentConfig or None def __init__(self, parent=None, attachment_type: str = ""):
# 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"
"""
super().__init__(parent) super().__init__(parent)
self.attachment_type = attachment_type self.preferred_type = attachment_type.lower()
self.selected_attachment = None
self.allow_none = allow_none
type_name = self.TYPE_NAMES.get(attachment_type, attachment_type.title()) type_names = {
self.setWindowTitle(f"Select {type_name}") "amplifier": "Amplifier",
self.setMinimumSize(500, 450) "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._setup_ui()
self._load_attachments() self._load_data()
def _setup_ui(self): def _setup_ui(self):
"""Setup the UI."""
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setSpacing(10) layout.setSpacing(10)
# Header # Status
type_name = self.TYPE_NAMES.get(self.attachment_type, self.attachment_type.title()) self.status_label = QLabel("Loading attachments from Entropia Nexus...")
type_icon = self.TYPE_ICONS.get(self.attachment_type, '📎') layout.addWidget(self.status_label)
header = QLabel(f"{type_icon} Select {type_name}") self.progress = QProgressBar()
header.setFont(QFont("Arial", 14, QFont.Weight.Bold)) self.progress.setRange(0, 0)
header.setStyleSheet("color: #4a90d9; padding: 5px;") layout.addWidget(self.progress)
layout.addWidget(header)
# Description # Type filter tabs
desc = self._get_type_description() self.tabs = QTabWidget()
if desc:
desc_label = QLabel(desc)
desc_label.setStyleSheet("color: #888888; padding-bottom: 10px;")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
# Attachment list # Create tabs for each attachment type
self.list_widget = QListWidget() self.tab_all = self._create_attachment_tab("All Attachments")
self.list_widget.setAlternatingRowColors(True) self.tab_amps = self._create_attachment_tab("Amplifiers")
self.list_widget.setStyleSheet(""" self.tab_scopes = self._create_attachment_tab("Scopes")
QListWidget { self.tab_sights = self._create_attachment_tab("Sights")
background-color: #2d2d2d; self.tab_absorbers = self._create_attachment_tab("Absorbers")
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)
# Stats preview self.tabs.addTab(self.tab_all["widget"], "All")
self.preview_group = QWidget() 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 = QFormLayout(self.preview_group)
preview_layout.setContentsMargins(10, 10, 10, 10) self.preview_name = QLabel("-")
self.preview_group.setStyleSheet(""" self.preview_type = QLabel("-")
QWidget { self.preview_damage = QLabel("-")
background-color: #2d2d2d; self.preview_range = QLabel("-")
border-radius: 4px; self.preview_decay = QLabel("-")
} self.preview_efficiency = QLabel("-")
QLabel { preview_layout.addRow("Name:", self.preview_name)
color: #e0e0e0; 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)
self.preview_name = QLabel("No attachment selected") preview_layout.addRow("Efficiency:", self.preview_efficiency)
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)
layout.addWidget(self.preview_group) layout.addWidget(self.preview_group)
# Buttons # Buttons
button_layout = QHBoxLayout() 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)
if self.allow_none: def _create_attachment_tab(self, title: str) -> dict:
self.none_btn = QPushButton("❌ Remove Attachment") """Create a tab with search and tree for attachments."""
self.none_btn.setStyleSheet(""" widget = QWidget()
QPushButton { layout = QVBoxLayout(widget)
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() # Search
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:"))
search_input = QLineEdit()
search_input.setPlaceholderText(f"Search {title.lower()}...")
search_layout.addWidget(search_input)
self.ok_btn = QPushButton("✓ Select") clear_btn = QPushButton("Clear")
self.ok_btn.setEnabled(False) search_layout.addWidget(clear_btn)
self.ok_btn.setStyleSheet(""" layout.addLayout(search_layout)
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") # Tree
cancel_btn.clicked.connect(self.reject) 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)
button_layout.addWidget(self.ok_btn) return {
button_layout.addWidget(cancel_btn) "widget": widget,
layout.addLayout(button_layout) "search": search_input,
"clear": clear_btn,
def _get_type_description(self) -> str: "tree": tree
"""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.',
} }
return descriptions.get(self.attachment_type, "")
def _load_attachments(self): def _load_data(self):
"""Load attachments from data source.""" """Load attachments in background thread."""
attachments = get_mock_attachments(self.attachment_type) self.loader = AttachmentLoaderThread()
self.attachments = attachments self.loader.attachments_loaded.connect(self._on_attachments_loaded)
self.loader.error_occurred.connect(self._on_load_error)
self.loader.start()
self.list_widget.clear() 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: for att in attachments:
icon = self.TYPE_ICONS.get(self.attachment_type, '📎') item = QTreeWidgetItem()
item = QListWidgetItem(f"{icon} {att.name}") item.setText(0, att.name)
item.setData(Qt.ItemDataRole.UserRole, att) 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 # Color code by type
tooltip_lines = [f"Decay: {att.decay_pec} PEC/shot"] 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): item.setData(0, Qt.ItemDataRole.UserRole, att)
tooltip_lines.append(f"Damage: +{att.damage_increase}") tree.addTopLevelItem(item)
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)) def _on_search_changed(self, tab: dict, text: str):
self.list_widget.addItem(item) """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): def _on_selection_changed(self):
"""Handle selection change.""" """Handle selection change in current tab."""
selected = self.list_widget.selectedItems() current_tab = self._get_current_tab()
if selected: if current_tab:
attachment = selected[0].data(Qt.ItemDataRole.UserRole) items = current_tab["tree"].selectedItems()
self.selected_attachment = attachment if items:
self.ok_btn.setEnabled(True) self.selected_attachment = items[0].data(0, Qt.ItemDataRole.UserRole)
self._update_preview(attachment) self.ok_button.setEnabled(True)
else: self._update_preview(self.selected_attachment)
self.selected_attachment = None else:
self.ok_btn.setEnabled(False) self.selected_attachment = None
self.preview_name.setText("No attachment selected") self.ok_button.setEnabled(False)
self.preview_stats.setText("") 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.""" """Update preview panel."""
self.preview_name.setText(attachment.name) self.preview_name.setText(attachment.name)
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}%")
stats_lines = [] def _clear_preview(self):
stats_lines.append(f"<b>Decay:</b> {attachment.decay_pec} PEC/shot") """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("-")
if isinstance(attachment, WeaponAmplifier): def _on_double_click(self, item, column):
stats_lines.append(f"<b>Damage Increase:</b> +{attachment.damage_increase}")
stats_lines.append(f"<b>Ammo Increase:</b> +{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"<b>Amp DPP:</b> {amp_dpp:.2f}")
elif isinstance(attachment, WeaponScope):
stats_lines.append(f"<b>Range Increase:</b> +{attachment.range_increase}m")
if attachment.accuracy_bonus > 0:
stats_lines.append(f"<b>Accuracy Bonus:</b> +{attachment.accuracy_bonus}")
elif isinstance(attachment, WeaponAbsorber):
stats_lines.append(f"<b>Damage Reduction:</b> -{attachment.damage_reduction}")
elif isinstance(attachment, ArmorPlating):
total = attachment.get_total_protection()
stats_lines.append(f"<b>Total Protection:</b> +{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"<b>Protection:</b> {', '.join(prots)}")
elif isinstance(attachment, FinderAmplifier):
stats_lines.append(f"<b>Depth:</b> +{attachment.depth_increase}m")
stats_lines.append(f"<b>Radius:</b> +{attachment.radius_increase}m")
elif isinstance(attachment, Enhancer):
stats_lines.append(f"<b>Tier:</b> {attachment.tier}")
stats_lines.append(f"<b>Effect:</b> {attachment.effect_name} +{attachment.effect_value}")
elif isinstance(attachment, MindforceImplant):
stats_lines.append(f"<b>Mindforce Bonus:</b> +{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"<b>Cost/Hour:</b> {cost_per_hour:.2f} PED")
self.preview_stats.setText("<br>".join(stats_lines))
def _on_double_click(self, item):
"""Handle double click.""" """Handle double click."""
self._on_accept() self._on_accept()
def _on_accept(self): def _on_accept(self):
"""Handle OK button.""" """Handle OK button."""
if self.selected_attachment: if self.selected_attachment:
att = self.selected_attachment self.attachment_selected.emit(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.accept() 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()

262
ui/plate_selector.py Normal file
View File

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