""" Attachment Selector Dialog for Lemontropia Suite UI for selecting and managing gear attachments. """ 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 ) # ============================================================================ # Data Class for Attachment Configuration # ============================================================================ 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. 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(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" """ super().__init__(parent) self.attachment_type = attachment_type self.selected_attachment = None self.allow_none = allow_none type_name = self.TYPE_NAMES.get(attachment_type, attachment_type.title()) self.setWindowTitle(f"Select {type_name}") self.setMinimumSize(500, 450) self._setup_ui() self._load_attachments() 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, '📎') 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) # 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) # 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) # Stats preview self.preview_group = QWidget() 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) 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) 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.', } 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 self.list_widget.clear() for att in attachments: icon = self.TYPE_ICONS.get(self.attachment_type, '📎') item = QListWidgetItem(f"{icon} {att.name}") item.setData(Qt.ItemDataRole.UserRole, att) # Build tooltip tooltip_lines = [f"Decay: {att.decay_pec} PEC/shot"] 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) 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("") def _update_preview(self, attachment): """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)) def _on_double_click(self, item): """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.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()