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:
parent
649aa77bc9
commit
1c0d684c4c
|
|
@ -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()
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue