""" Lemontropia Suite - Loadout Manager UI v3.0 Complete armor system with sets, individual pieces, and plating. """ import json import os import logging from dataclasses import dataclass, asdict, field from decimal import Decimal, InvalidOperation from pathlib import Path from typing import Optional, List, Dict, Any from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QComboBox, QLabel, QPushButton, QGroupBox, QSpinBox, QMessageBox, QListWidget, QListWidgetItem, QSplitter, QWidget, QFrame, QScrollArea, QGridLayout, QCheckBox, QDialogButtonBox, QTreeWidget, QTreeWidgetItem, QHeaderView, QTabWidget, QProgressDialog, QStackedWidget, QSizePolicy ) from PyQt6.QtCore import Qt, pyqtSignal, QThread from PyQt6.QtGui import QFont from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats from core.nexus_full_api import ( get_nexus_api, NexusArmor, NexusArmorSet, NexusHealingTool, NexusPlate, NexusAttachment, NexusEnhancer, NexusRing, NexusClothing, NexusPet ) from core.attachments import ( Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber, ArmorPlating, Enhancer, can_attach, get_mock_attachments ) from core.armor_system import ( ArmorSlot, ArmorSet, ArmorPiece, ArmorPlate, EquippedArmor, ProtectionProfile, HitResult, calculate_hit_protection, get_all_armor_sets, get_all_armor_pieces, get_pieces_by_slot, get_mock_plates, format_protection, ALL_ARMOR_SLOTS, create_ghost_set, create_shogun_set, create_vigilante_set, create_hermes_set, create_pixie_set, ) logger = logging.getLogger(__name__) # ============================================================================ # Data Structures # ============================================================================ @dataclass class AttachmentConfig: """Configuration for an equipped attachment.""" 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: Dict[str, Decimal] = field(default_factory=dict) def to_dict(self) -> dict: 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()}, } @classmethod def from_dict(cls, data: dict) -> "AttachmentConfig": 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()}, ) @dataclass class LoadoutConfig: """Configuration for a hunting loadout with full armor system.""" name: str # Weapon weapon_name: str weapon_id: int = 0 weapon_damage: Decimal = Decimal("0") weapon_decay_pec: Decimal = Decimal("0") weapon_ammo_pec: Decimal = Decimal("0") weapon_dpp: Decimal = Decimal("0") # Weapon Attachments weapon_amplifier: Optional[AttachmentConfig] = None weapon_scope: Optional[AttachmentConfig] = None weapon_absorber: Optional[AttachmentConfig] = None # Weapon Enhancers (up to 10 slots) weapon_enhancers: List[AttachmentConfig] = field(default_factory=list) # Armor System equipped_armor: Optional[EquippedArmor] = None armor_set_name: str = "-- None --" # Armor Plates (per slot) armor_plates: Dict[str, AttachmentConfig] = field(default_factory=dict) # Legacy armor fields for backward compatibility armor_name: str = "-- None --" armor_id: int = 0 armor_decay_pec: Decimal = Decimal("0") protection_stab: Decimal = Decimal("0") protection_cut: Decimal = Decimal("0") protection_impact: Decimal = Decimal("0") protection_penetration: Decimal = Decimal("0") protection_shrapnel: Decimal = Decimal("0") protection_burn: Decimal = Decimal("0") protection_cold: Decimal = Decimal("0") protection_acid: Decimal = Decimal("0") protection_electric: Decimal = Decimal("0") # Healing heal_name: str = "-- Custom --" heal_cost_pec: Decimal = Decimal("2.0") heal_amount: Decimal = Decimal("20") # Accessories left_ring: Optional[str] = None right_ring: Optional[str] = None clothing_items: List[str] = field(default_factory=list) pet: Optional[str] = None # Mindforce mindforce_implant: Optional[str] = None mindforce_decay_pec: Decimal = Decimal("0") # Enhancers - tier-based (1 per tier, max 10 tiers) # Format: {tier_number: enhancer} enhancers: Dict[int, 'NexusEnhancer'] = field(default_factory=dict) # Settings shots_per_hour: int = 3600 hits_per_hour: int = 720 heals_per_hour: int = 60 def get_total_damage(self) -> Decimal: """Calculate total damage including amplifier.""" base = self.weapon_damage if self.weapon_amplifier: base += self.weapon_amplifier.damage_bonus return base def get_total_decay_per_shot(self) -> Decimal: """Calculate total decay per shot including attachments and enhancers.""" total = self.weapon_decay_pec if self.weapon_amplifier: total += self.weapon_amplifier.decay_pec if self.weapon_scope: total += self.weapon_scope.decay_pec if self.weapon_absorber: total += self.weapon_absorber.decay_pec # Add enhancer decay from all tiers for tier, enhancer in self.enhancers.items(): total += enhancer.decay # Add mindforce decay if used total += self.mindforce_decay_pec return total def get_total_ammo_per_shot(self) -> Decimal: """Calculate total ammo cost per shot in PEC.""" total = self.weapon_ammo_pec * Decimal("0.01") if self.weapon_amplifier: total += self.weapon_amplifier.damage_bonus * Decimal("0.2") return total def calculate_dpp(self) -> Decimal: """Calculate Damage Per Pec (DPP) with all attachments.""" total_damage = self.get_total_damage() total_cost = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot() if total_cost == 0: return Decimal("0") return total_damage / total_cost def calculate_dps(self) -> Decimal: """Calculate Damage Per Second (DPS) with all attachments.""" # Standard EU fire rate is typically 1 shot per second without attachments # A scope may increase this, but we'll use a base of 1.0 for simplicity shots_per_second = Decimal("1.0") if self.weapon_scope: # Scopes can increase fire rate, but let's be conservative shots_per_second = Decimal("1.2") return self.get_total_damage() * shots_per_second def get_armor_decay_per_hit(self) -> Decimal: """Calculate armor decay cost per hit taken (in PED).""" decay_per_hit = Decimal("0") if self.equipped_armor: decay_per_hit = self.equipped_armor.get_total_decay_per_hit() else: # Legacy fallback decay_per_hit = self.armor_decay_pec # Add plate decay costs for slot, plate_config in self.armor_plates.items(): decay_per_hit += plate_config.decay_pec return decay_per_hit / Decimal("100") # Convert PEC to PED def get_heal_cost_per_use(self) -> Decimal: """Calculate healing cost per use (in PED).""" return self.heal_cost_pec / Decimal("100") def get_hp_per_pec(self) -> Decimal: """Calculate HP healed per PEC spent.""" if self.heal_cost_pec == 0: return Decimal("0") return self.heal_amount / self.heal_cost_pec def calculate_weapon_cost_per_hour(self) -> Decimal: """Calculate weapon cost per hour.""" cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot() return cost_per_shot * Decimal(self.shots_per_hour) def calculate_armor_cost_per_hour(self) -> Decimal: """Calculate armor cost per hour including plates.""" base_cost = Decimal("0") if self.equipped_armor: base_cost = self.equipped_armor.get_total_decay_per_hit() * Decimal(self.hits_per_hour) else: # Legacy fallback base_cost = self.armor_decay_pec * Decimal(self.hits_per_hour) # Add plate decay costs for slot, plate_config in self.armor_plates.items(): base_cost += plate_config.decay_pec * Decimal(self.hits_per_hour) return base_cost def calculate_heal_cost_per_hour(self) -> Decimal: """Calculate healing cost per hour.""" return self.heal_cost_pec * Decimal(self.heals_per_hour) def calculate_total_cost_per_hour(self) -> Decimal: """Calculate total PED cost per hour.""" weapon_cost = self.calculate_weapon_cost_per_hour() armor_cost = self.calculate_armor_cost_per_hour() heal_cost = self.calculate_heal_cost_per_hour() total_pec = weapon_cost + armor_cost + heal_cost return total_pec / Decimal("100") def calculate_break_even(self, mob_health: Decimal) -> Decimal: """Calculate break-even loot value for a mob.""" total_damage = self.get_total_damage() shots_to_kill = mob_health / total_damage if total_damage > 0 else Decimal("1") if shots_to_kill < 1: shots_to_kill = Decimal("1") cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot() total_cost_pec = shots_to_kill * cost_per_shot return total_cost_pec / Decimal("100") def get_total_protection(self) -> ProtectionProfile: """Get total protection from equipped armor.""" if self.equipped_armor: return self.equipped_armor.get_total_protection() # Legacy fallback return ProtectionProfile( stab=self.protection_stab, cut=self.protection_cut, impact=self.protection_impact, penetration=self.protection_penetration, shrapnel=self.protection_shrapnel, burn=self.protection_burn, cold=self.protection_cold, acid=self.protection_acid, electric=self.protection_electric, ) def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" data = { k: str(v) if isinstance(v, Decimal) else v for k, v in asdict(self).items() } # Handle attachment configs if self.weapon_amplifier: data['weapon_amplifier'] = self.weapon_amplifier.to_dict() if self.weapon_scope: data['weapon_scope'] = self.weapon_scope.to_dict() if self.weapon_absorber: data['weapon_absorber'] = self.weapon_absorber.to_dict() # Handle weapon enhancers if self.weapon_enhancers: data['weapon_enhancers'] = [e.to_dict() for e in self.weapon_enhancers] # Handle armor plates if self.armor_plates: data['armor_plates'] = {k: v.to_dict() for k, v in self.armor_plates.items()} # Handle equipped armor if self.equipped_armor: data['equipped_armor'] = self.equipped_armor.to_dict() return data @classmethod def from_dict(cls, data: dict) -> "LoadoutConfig": """Create LoadoutConfig from dictionary.""" decimal_fields = [ 'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec', 'weapon_dpp', 'armor_decay_pec', 'heal_cost_pec', 'heal_amount', 'protection_stab', 'protection_cut', 'protection_impact', 'protection_penetration', 'protection_shrapnel', 'protection_burn', 'protection_cold', 'protection_acid', 'protection_electric' ] for field in decimal_fields: if field in data: data[field] = Decimal(data[field]) # Handle integer fields int_fields = ['weapon_id', 'armor_id', 'shots_per_hour', 'hits_per_hour', 'heals_per_hour'] for field in int_fields: if field in data: data[field] = int(data[field]) # Handle attachment configs if 'weapon_amplifier' in data and data['weapon_amplifier']: data['weapon_amplifier'] = AttachmentConfig.from_dict(data['weapon_amplifier']) else: data['weapon_amplifier'] = None if 'weapon_scope' in data and data['weapon_scope']: data['weapon_scope'] = AttachmentConfig.from_dict(data['weapon_scope']) else: data['weapon_scope'] = None if 'weapon_absorber' in data and data['weapon_absorber']: data['weapon_absorber'] = AttachmentConfig.from_dict(data['weapon_absorber']) else: data['weapon_absorber'] = None # Handle weapon enhancers if 'weapon_enhancers' in data and data['weapon_enhancers']: data['weapon_enhancers'] = [AttachmentConfig.from_dict(e) for e in data['weapon_enhancers']] else: data['weapon_enhancers'] = [] # Handle armor plates if 'armor_plates' in data and data['armor_plates']: data['armor_plates'] = {k: AttachmentConfig.from_dict(v) for k, v in data['armor_plates'].items()} else: data['armor_plates'] = {} # Handle accessories if 'clothing_items' not in data: data['clothing_items'] = [] if 'left_ring' not in data: data['left_ring'] = None if 'right_ring' not in data: data['right_ring'] = None if 'pet' not in data: data['pet'] = None # Handle equipped armor if 'equipped_armor' in data and data['equipped_armor']: data['equipped_armor'] = EquippedArmor.from_dict(data['equipped_armor']) else: data['equipped_armor'] = None # Handle legacy configs if 'heal_name' not in data: data['heal_name'] = '-- Custom --' if 'armor_set_name' not in data: data['armor_set_name'] = '-- None --' return cls(**data) # ============================================================================ # Healing Tools Data - Using real database # ============================================================================ def get_healing_tools_data(): """Get healing tools from the real database.""" try: from core.healing_tools import HEALING_TOOLS return [ { "name": tool.name, "cost": tool.decay_pec, "amount": tool.heal_amount, "is_chip": tool.is_chip } for tool in HEALING_TOOLS ] except ImportError: # Fallback to mock data if import fails return [ {"name": "Vivo T10", "cost": Decimal("0.815"), "amount": Decimal("10")}, {"name": "Vivo S10", "cost": Decimal("1.705"), "amount": Decimal("21")}, {"name": "Hedoc MM10", "cost": Decimal("2.09"), "amount": Decimal("44")}, {"name": "Adjusted Restoration Chip", "cost": Decimal("2.88"), "amount": Decimal("60")}, {"name": "Restoration Chip IV (L)", "cost": Decimal("2.8"), "amount": Decimal("45")}, ] # Legacy mock data for compatibility MOCK_HEALING = get_healing_tools_data() # ============================================================================ # Custom Widgets # ============================================================================ class DecimalLineEdit(QLineEdit): """Line edit with decimal validation.""" def __init__(self, parent=None): super().__init__(parent) self.setPlaceholderText("0.00") def get_decimal(self) -> Decimal: """Get value as Decimal, returns 0 on invalid input.""" text = self.text().strip() if not text: return Decimal("0") try: return Decimal(text) except InvalidOperation: return Decimal("0") def set_decimal(self, value: Decimal): """Set value from Decimal.""" self.setText(str(value)) class DarkGroupBox(QGroupBox): """Group box with dark theme styling.""" def __init__(self, title: str, parent=None): super().__init__(title, parent) self.setStyleSheet(""" QGroupBox { color: #e0e0e0; border: 2px solid #3d3d3d; border-radius: 6px; margin-top: 10px; padding-top: 10px; font-weight: bold; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } """) class ArmorSlotWidget(QWidget): """Widget for configuring a single armor slot with piece and plate.""" piece_changed = pyqtSignal() plate_changed = pyqtSignal() def __init__(self, slot: ArmorSlot, parent=None): super().__init__(parent) self.slot = slot self.current_piece: Optional[ArmorPiece] = None self.current_plate: Optional[ArmorPlate] = None self._setup_ui() def _setup_ui(self): layout = QHBoxLayout(self) layout.setContentsMargins(5, 2, 5, 2) layout.setSpacing(10) slot_name = self._get_slot_display_name() # Slot label self.slot_label = QLabel(f"{slot_name}:") self.slot_label.setFixedWidth(100) layout.addWidget(self.slot_label) # Armor piece selector self.piece_combo = QComboBox() self.piece_combo.setMinimumWidth(180) self.piece_combo.currentTextChanged.connect(self._on_piece_changed) layout.addWidget(self.piece_combo) # Protection display self.protection_label = QLabel("-") self.protection_label.setStyleSheet("color: #888888; font-size: 11px;") self.protection_label.setFixedWidth(120) layout.addWidget(self.protection_label) # Plate selector self.plate_combo = QComboBox() self.plate_combo.setMinimumWidth(150) self.plate_combo.currentTextChanged.connect(self._on_plate_changed) layout.addWidget(self.plate_combo) # Add plate search button self.search_plate_btn = QPushButton("šŸ”") self.search_plate_btn.setToolTip("Search plates from Nexus API") self.search_plate_btn.setFixedWidth(40) self.search_plate_btn.clicked.connect(self._on_search_plate) layout.addWidget(self.search_plate_btn) # Total protection self.total_label = QLabel("Total: 0") self.total_label.setStyleSheet("color: #4caf50; font-weight: bold;") self.total_label.setFixedWidth(80) layout.addWidget(self.total_label) layout.addStretch() # Populate combos self._populate_pieces() self._populate_plates() def _get_slot_display_name(self) -> str: """Get human-readable slot name (matches Entropia Nexus).""" names = { ArmorSlot.HEAD: "Head", ArmorSlot.TORSO: "Torso", ArmorSlot.ARMS: "Arms", ArmorSlot.HANDS: "Hands", ArmorSlot.LEGS: "Legs", ArmorSlot.SHINS: "Shins", ArmorSlot.FEET: "Feet", } return names.get(self.slot, self.slot.value) def _populate_pieces(self): """Populate armor piece combo.""" self.piece_combo.clear() self.piece_combo.addItem("-- Empty --") # Get pieces for this slot pieces = get_pieces_by_slot(self.slot) for piece in pieces: display = f"{piece.name} ({piece.set_name})" self.piece_combo.addItem(display, piece) def _populate_plates(self): """Populate plate combo.""" self.plate_combo.clear() self.plate_combo.addItem("-- No Plate --") plates = get_mock_plates() for plate in plates: display = f"{plate.name} (+{plate.get_total_protection()})" self.plate_combo.addItem(display, plate) def _on_search_plate(self): """Open plate selector dialog from Nexus API.""" from ui.plate_selector import PlateSelectorDialog # Get current piece's protection to suggest matching plates preferred_type = "" if self.current_piece: # Find highest protection type protections = { 'impact': self.current_piece.protection.impact, 'cut': self.current_piece.protection.cut, 'stab': self.current_piece.protection.stab, 'burn': self.current_piece.protection.burn, 'cold': self.current_piece.protection.cold, } preferred_type = max(protections, key=protections.get) dialog = PlateSelectorDialog(self, damage_type=preferred_type) dialog.plate_selected.connect(self._on_api_plate_selected) dialog.exec() def _on_api_plate_selected(self, plate: NexusPlate): """Handle plate selection from API.""" # Add to combo if not exists index = self.plate_combo.findText(plate.name) if index < 0: # Create ArmorPlate from NexusPlate from core.armor_system import ArmorPlate as LocalArmorPlate local_plate = LocalArmorPlate( name=plate.name, item_id=plate.item_id, protection=plate.protection, durability=plate.durability, decay_per_hp=plate.decay_per_hp ) display = f"{plate.name} (+{plate.protection_impact + plate.protection_cut + plate.protection_stab} prot)" self.plate_combo.addItem(display, local_plate) index = self.plate_combo.count() - 1 self.plate_combo.setCurrentIndex(index) self._update_total() def _on_piece_changed(self, text: str): """Handle armor piece selection.""" if text == "-- Empty --": self.current_piece = None self.protection_label.setText("-") else: self.current_piece = self.piece_combo.currentData() if self.current_piece: prot = format_protection(self.current_piece.protection) self.protection_label.setText(prot) self._update_total() self.piece_changed.emit() def _on_plate_changed(self, text: str): """Handle plate selection.""" if text == "-- No Plate --": self.current_plate = None else: self.current_plate = self.plate_combo.currentData() self._update_total() self.plate_changed.emit() def _update_total(self): """Update total protection display.""" total = Decimal("0") if self.current_piece: total += self.current_piece.protection.get_total() if self.current_plate: total += self.current_plate.get_total_protection() self.total_label.setText(f"Total: {total}") def get_piece(self) -> Optional[ArmorPiece]: """Get selected armor piece.""" return self.current_piece def get_plate(self) -> Optional[ArmorPlate]: """Get selected plate.""" return self.current_plate def set_piece(self, piece: Optional[ArmorPiece]): """Set selected armor piece.""" if piece is None: self.piece_combo.setCurrentIndex(0) self.current_piece = None self.protection_label.setText("-") self._update_total() return # Store the piece self.current_piece = piece # Update protection display prot_parts = [] if piece.protection.impact > 0: prot_parts.append(f"Imp:{piece.protection.impact}") if piece.protection.cut > 0: prot_parts.append(f"Cut:{piece.protection.cut}") if piece.protection.stab > 0: prot_parts.append(f"Stab:{piece.protection.stab}") self.protection_label.setText(", ".join(prot_parts) if prot_parts else "-") # Try to find and select the piece in combo for i in range(self.piece_combo.count()): data = self.piece_combo.itemData(i) if data and hasattr(data, 'item_id') and data.item_id == piece.item_id: self.piece_combo.setCurrentIndex(i) self._update_total() return # Piece not in combo - add it display = f"{piece.name} (API)" self.piece_combo.addItem(display, piece) self.piece_combo.setCurrentIndex(self.piece_combo.count() - 1) self._update_total() def set_plate(self, plate: Optional[ArmorPlate]): """Set selected plate.""" if plate is None: self.plate_combo.setCurrentIndex(0) return # Find and select the plate for i in range(self.plate_combo.count()): data = self.plate_combo.itemData(i) if data and data.item_id == plate.item_id: self.plate_combo.setCurrentIndex(i) return self.plate_combo.setCurrentIndex(0) def get_total_protection(self) -> ProtectionProfile: """Get total protection for this slot.""" total = ProtectionProfile() if self.current_piece: total = total.add(self.current_piece.protection) if self.current_plate: total = total.add(self.current_plate.protection) return total def get_total_decay(self) -> Decimal: """Get total decay per hit for this slot (estimated).""" # Estimate based on typical hit of 10 hp typical_hit = Decimal("10") decay = Decimal("0") if self.current_piece: # Armor only decays for damage it actually absorbs armor_absorb = min(typical_hit, self.current_piece.protection.get_total()) decay += self.current_piece.get_decay_for_damage(armor_absorb) if self.current_plate: # Plate only decays for damage it actually absorbs plate_absorb = min(typical_hit, self.current_plate.get_total_protection()) decay += self.current_plate.get_decay_for_damage(plate_absorb) return decay # ============================================================================ # Gear Loader Threads # ============================================================================ class WeaponLoaderThread(QThread): """Thread to load weapons from API.""" weapons_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def run(self): try: api = EntropiaNexusAPI() weapons = api.get_all_weapons() self.weapons_loaded.emit(weapons) except Exception as e: logger.error(f"Failed to load weapons: {e}") self.error_occurred.emit(str(e)) class ArmorLoaderThread(QThread): """Thread to load armors from API.""" armors_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def run(self): try: api = EntropiaNexusAPI() armors = api.get_all_armors() self.armors_loaded.emit(armors) except Exception as e: logger.error(f"Failed to load armors: {e}") self.error_occurred.emit(str(e)) # ============================================================================ # Weapon Selector Dialog # ============================================================================ class WeaponSelectorDialog(QDialog): """Dialog for selecting weapons from Entropia Nexus API.""" weapon_selected = pyqtSignal(object) def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Select Weapon - Entropia Nexus") self.setMinimumSize(900, 600) self.weapons = [] self.selected_weapon = None self.api = EntropiaNexusAPI() self._setup_ui() self._load_data() def _setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) self.status_label = QLabel("Loading weapons from Entropia Nexus...") layout.addWidget(self.status_label) search_layout = QHBoxLayout() search_layout.addWidget(QLabel("Search:")) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search weapons by name...") self.search_input.returnPressed.connect(self._on_search) search_layout.addWidget(self.search_input) self.search_btn = QPushButton("Search") self.search_btn.clicked.connect(self._on_search) search_layout.addWidget(self.search_btn) layout.addLayout(search_layout) self.results_tree = QTreeWidget() self.results_tree.setHeaderLabels([ "Name", "Type", "Category", "Damage", "DPP", "Decay", "Ammo", "Cost/h" ]) header = self.results_tree.header() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) for i in range(1, 8): header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed) header.resizeSection(1, 80) header.resizeSection(2, 80) header.resizeSection(3, 60) header.resizeSection(4, 60) header.resizeSection(5, 70) header.resizeSection(6, 60) header.resizeSection(7, 70) self.results_tree.setAlternatingRowColors(True) self.results_tree.itemSelectionChanged.connect(self._on_selection_changed) self.results_tree.itemDoubleClicked.connect(self._on_double_click) layout.addWidget(self.results_tree) self.preview_group = DarkGroupBox("Weapon Stats") self.preview_layout = QFormLayout(self.preview_group) self.preview_layout.addRow("Select a weapon to view stats", QLabel("")) layout.addWidget(self.preview_group) button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self._on_accept) button_box.rejected.connect(self.reject) self.ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok) self.ok_btn.setEnabled(False) self.ok_btn.setText("Select Weapon") layout.addWidget(button_box) def _load_data(self): """Load weapons asynchronously.""" self.loader = WeaponLoaderThread() self.loader.weapons_loaded.connect(self._on_data_loaded) self.loader.error_occurred.connect(self._on_load_error) self.loader.start() def _on_data_loaded(self, weapons): """Handle loaded weapons.""" self.weapons = weapons self.status_label.setText(f"Loaded {len(weapons):,} weapons from Entropia Nexus") self._populate_tree(weapons[:200]) def _on_load_error(self, error): """Handle load error.""" self.status_label.setText(f"Error loading weapons: {error}") QMessageBox.critical(self, "Error", f"Failed to load weapons: {error}") def _populate_tree(self, weapons): """Populate tree with weapons.""" self.results_tree.clear() for w in weapons: item = QTreeWidgetItem([ w.name, w.type, w.category, str(w.total_damage), f"{w.dpp:.2f}", f"{w.decay:.2f}" if w.decay else "-", str(w.ammo_burn) if w.ammo_burn else "-", f"{w.cost_per_hour:.0f}" ]) item.setData(0, Qt.ItemDataRole.UserRole, w) self.results_tree.addTopLevelItem(item) def _on_search(self): """Search weapons.""" query = self.search_input.text().strip().lower() if not query: self._populate_tree(self.weapons[:200]) return results = [w for w in self.weapons if query in w.name.lower()] self._populate_tree(results) self.status_label.setText(f"Found {len(results)} weapons matching '{query}'") def _on_selection_changed(self): """Handle selection change.""" selected = self.results_tree.selectedItems() if selected: weapon = selected[0].data(0, Qt.ItemDataRole.UserRole) self.selected_weapon = weapon self.ok_btn.setEnabled(True) self._update_preview(weapon) else: self.selected_weapon = None self.ok_btn.setEnabled(False) def _update_preview(self, w): """Update stats preview.""" while self.preview_layout.rowCount() > 0: self.preview_layout.removeRow(0) self.preview_layout.addRow("Name:", QLabel(w.name)) self.preview_layout.addRow("Type:", QLabel(f"{w.type} {w.category}")) self.preview_layout.addRow("Damage:", QLabel(str(w.total_damage))) self.preview_layout.addRow("DPP:", QLabel(f"{w.dpp:.3f}")) self.preview_layout.addRow("Decay:", QLabel(f"{w.decay:.3f} PEC/shot" if w.decay else "-")) self.preview_layout.addRow("Ammo:", QLabel(f"{w.ammo_burn} units/shot" if w.ammo_burn else "-")) self.preview_layout.addRow("Cost/Hour:", QLabel(f"{w.cost_per_hour:.2f} PED")) if w.efficiency: self.preview_layout.addRow("Efficiency:", QLabel(f"{w.efficiency:.1f}%")) def _on_double_click(self, item, column): """Handle double click.""" self._on_accept() def _on_accept(self): """Handle OK button.""" if self.selected_weapon: self.weapon_selected.emit(self.selected_weapon) self.accept() # ============================================================================ # Armor Selector Dialog # ============================================================================ class ArmorSelectorDialog(QDialog): """Dialog for selecting armors from Entropia Nexus API.""" armor_selected = pyqtSignal(object) def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Select Armor - Entropia Nexus") self.setMinimumSize(900, 600) self.armors = [] self.selected_armor = None self.api = EntropiaNexusAPI() self._setup_ui() self._load_data() def _setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) self.status_label = QLabel("Loading armors from Entropia Nexus...") layout.addWidget(self.status_label) search_layout = QHBoxLayout() search_layout.addWidget(QLabel("Search:")) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search armors by name...") self.search_input.returnPressed.connect(self._on_search) search_layout.addWidget(self.search_input) self.search_btn = QPushButton("Search") self.search_btn.clicked.connect(self._on_search) search_layout.addWidget(self.search_btn) layout.addLayout(search_layout) self.results_tree = QTreeWidget() self.results_tree.setHeaderLabels([ "Name", "Type", "Durability", "Impact", "Cut", "Stab", "Burn", "Cold" ]) 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) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) def _load_data(self): """Load armors from API.""" try: self.armors = self.api.get_all_armors() self._populate_results(self.armors) self.status_label.setText(f"Loaded {len(self.armors)} armors from Entropia Nexus") except Exception as e: self.status_label.setText(f"Error loading armors: {e}") def _populate_results(self, armors): """Populate results tree.""" self.results_tree.clear() for armor in armors: item = QTreeWidgetItem() item.setText(0, armor.name) item.setText(1, armor.type or "Unknown") item.setText(2, str(armor.durability)) item.setText(3, str(armor.protection_impact)) item.setText(4, str(armor.protection_cut)) item.setText(5, str(armor.protection_stab)) item.setText(6, str(armor.protection_burn)) item.setText(7, str(armor.protection_cold)) item.setData(0, Qt.ItemDataRole.UserRole, armor) self.results_tree.addTopLevelItem(item) def _on_search(self): """Handle search.""" query = self.search_input.text().lower() if not query: self._populate_results(self.armors) return filtered = [a for a in self.armors if query in a.name.lower()] self._populate_results(filtered) self.status_label.setText(f"Found {len(filtered)} armors matching '{query}'") def _on_selection_changed(self): """Handle selection change.""" items = self.results_tree.selectedItems() if items: self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole) def _on_double_click(self, item, column): """Handle double click.""" self._on_accept() def _on_accept(self): """Handle OK button.""" if self.selected_armor: self.armor_selected.emit(self.selected_armor) self.accept() # ============================================================================ # Main Loadout Manager Dialog # ============================================================================ class LoadoutManagerDialog(QDialog): """Main dialog for managing hunting loadouts with full armor system.""" loadout_saved = pyqtSignal(object) def __init__(self, parent=None, config_dir: Optional[str] = None): super().__init__(parent) self.setWindowTitle("Lemontropia Suite - Loadout Manager v3.0") self.setMinimumSize(1100, 900) if config_dir is None: self.config_dir = Path.home() / ".lemontropia" / "loadouts" else: self.config_dir = Path(config_dir) self.config_dir.mkdir(parents=True, exist_ok=True) self.current_loadout: Optional[LoadoutConfig] = None self.current_weapon: Optional[WeaponStats] = None self.current_armor_set: Optional[ArmorSet] = None self.equipped_armor: Optional[EquippedArmor] = None self.current_left_ring: Optional[NexusRing] = None self.current_right_ring: Optional[NexusRing] = None # Armor slot widgets self.slot_widgets: Dict[ArmorSlot, ArmorSlotWidget] = {} self._apply_dark_theme() self._create_widgets() self._create_layout() self._connect_signals() self._load_saved_loadouts() self._populate_armor_sets() self._populate_healing_data() def _apply_dark_theme(self): """Apply dark theme styling.""" self.setStyleSheet(""" QDialog { background-color: #1e1e1e; } QLabel { color: #e0e0e0; } QLineEdit { background-color: #2d2d2d; color: #e0e0e0; border: 1px solid #3d3d3d; border-radius: 4px; padding: 5px; } QLineEdit:disabled { background-color: #252525; color: #888888; border: 1px solid #2d2d2d; } QLineEdit:focus { border: 1px solid #4a90d9; } QComboBox { background-color: #2d2d2d; color: #e0e0e0; border: 1px solid #3d3d3d; border-radius: 4px; padding: 5px; min-width: 150px; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background-color: #2d2d2d; color: #e0e0e0; selection-background-color: #4a90d9; } QPushButton { background-color: #3d3d3d; color: #e0e0e0; border: 1px solid #4d4d4d; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #4d4d4d; } QPushButton:pressed { background-color: #5d5d5d; } QPushButton#saveButton { background-color: #2e7d32; border-color: #4caf50; } QPushButton#saveButton:hover { background-color: #4caf50; } QPushButton#deleteButton { background-color: #7d2e2e; border-color: #f44336; } QPushButton#deleteButton:hover { background-color: #f44336; } QPushButton#selectButton { background-color: #1565c0; border-color: #2196f3; } QPushButton#selectButton:hover { background-color: #2196f3; } QPushButton#clearButton { background-color: #5d4037; border-color: #8d6e63; } QPushButton#clearButton:hover { background-color: #8d6e63; } QListWidget { background-color: #2d2d2d; color: #e0e0e0; border: 1px solid #3d3d3d; border-radius: 4px; } QListWidget::item:selected { background-color: #4a90d9; } QScrollArea { border: none; } QTabWidget::pane { border: 1px solid #3d3d3d; background-color: #1e1e1e; } QTabBar::tab { background-color: #2d2d2d; color: #e0e0e0; padding: 8px 16px; border: 1px solid #3d3d3d; } QTabBar::tab:selected { background-color: #4a90d9; } """) def _create_widgets(self): """Create all UI widgets.""" # Loadout name self.loadout_name_edit = QLineEdit() self.loadout_name_edit.setPlaceholderText("Enter loadout name...") # Weapon section self.weapon_group = DarkGroupBox("šŸ”« Weapon Configuration") self.select_weapon_btn = QPushButton("šŸ” Select from Entropia Nexus") self.select_weapon_btn.setObjectName("selectButton") self.weapon_name_label = QLabel("No weapon selected") self.weapon_name_label.setStyleSheet("font-weight: bold; color: #4a90d9;") self.weapon_damage_edit = DecimalLineEdit() self.weapon_decay_edit = DecimalLineEdit() self.weapon_ammo_edit = DecimalLineEdit() self.dpp_label = QLabel("0.0000") self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 16px;") # Weapon attachments self.attach_amp_btn = QPushButton("⚔ Add Amplifier") self.attach_scope_btn = QPushButton("šŸ”­ Add Scope") self.attach_absorber_btn = QPushButton("šŸ›”ļø Add Absorber") self.amp_label = QLabel("None") self.scope_label = QLabel("None") self.absorber_label = QLabel("None") self.remove_amp_btn = QPushButton("āœ•") self.remove_scope_btn = QPushButton("āœ•") self.remove_absorber_btn = QPushButton("āœ•") self.remove_amp_btn.setFixedWidth(30) self.remove_scope_btn.setFixedWidth(30) self.remove_absorber_btn.setFixedWidth(30) # Armor section - NEW COMPLETE SYSTEM self.armor_group = DarkGroupBox("šŸ›”ļø Armor Configuration") # Armor set selector self.armor_set_combo = QComboBox() self.armor_set_combo.setMinimumWidth(250) self.equip_set_btn = QPushButton("Equip Full Set") self.equip_set_btn.setObjectName("selectButton") self.clear_armor_btn = QPushButton("Clear All") self.clear_armor_btn.setObjectName("clearButton") # Armor protection summary self.armor_summary_label = QLabel("No armor equipped") self.armor_summary_label.setStyleSheet("color: #888888; padding: 5px;") # Create slot widgets for slot in ALL_ARMOR_SLOTS: self.slot_widgets[slot] = ArmorSlotWidget(slot) self.slot_widgets[slot].piece_changed.connect(self._on_armor_changed) self.slot_widgets[slot].plate_changed.connect(self._on_armor_changed) # Healing section self.heal_group = DarkGroupBox("šŸ’Š Healing Configuration") self.heal_combo = QComboBox() self.heal_cost_edit = DecimalLineEdit() self.heal_amount_edit = DecimalLineEdit() # Cost summary - refined metrics (cost per action, not per hour) self.summary_group = DarkGroupBox("šŸ“Š Cost Analysis") # Weapon metrics self.cost_per_shot_label = QLabel("0.0000 PED") self.dps_label = QLabel("0.00 DPS") self.dpp_label = QLabel("0.0000 DPP") self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold;") # Armor metrics self.cost_per_hit_label = QLabel("0.0000 PED") self.protection_summary_label = QLabel("No protection") self.protection_summary_label.setStyleSheet("color: #4a90d9;") # Healing metrics self.cost_per_heal_label = QLabel("0.0000 PED") self.hp_per_pec_label = QLabel("0.00 HP/PEC") self.hp_per_pec_label.setStyleSheet("color: #4caf50;") # Total cost display self.total_cost_label = QLabel("0.0000 PED") self.total_cost_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 16px;") # Break-even calculator self.mob_health_edit = DecimalLineEdit() self.mob_health_edit.set_decimal(Decimal("100")) self.calc_break_even_btn = QPushButton("Calculate") self.break_even_label = QLabel("Break-even: 0.00 PED") self.break_even_label.setStyleSheet("color: #4caf50;") # Saved loadouts list self.saved_list = QListWidget() # Buttons self.save_btn = QPushButton("šŸ’¾ Save Loadout") self.save_btn.setObjectName("saveButton") self.load_btn = QPushButton("šŸ“‚ Load Selected") self.delete_btn = QPushButton("šŸ—‘ļø Delete") self.delete_btn.setObjectName("deleteButton") self.new_btn = QPushButton("šŸ†• New Loadout") self.close_btn = QPushButton("āŒ Close") self.refresh_btn = QPushButton("šŸ”„ Refresh") def _create_layout(self): """Create the main layout.""" main_layout = QHBoxLayout(self) main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # Left panel - Saved loadouts left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) saved_label = QLabel("šŸ’¼ Saved Loadouts") saved_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) left_layout.addWidget(saved_label) left_layout.addWidget(self.saved_list) left_btn_layout = QHBoxLayout() left_btn_layout.addWidget(self.load_btn) left_btn_layout.addWidget(self.delete_btn) left_layout.addLayout(left_btn_layout) left_layout.addWidget(self.refresh_btn) left_layout.addWidget(self.new_btn) left_layout.addStretch() left_layout.addWidget(self.close_btn) # Right panel - Configuration right_scroll = QScrollArea() right_scroll.setWidgetResizable(True) right_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_layout.setContentsMargins(0, 0, 10, 0) # Loadout name header name_layout = QHBoxLayout() name_label = QLabel("Loadout Name:") name_label.setFont(QFont("Arial", 10, QFont.Weight.Bold)) name_layout.addWidget(name_label) name_layout.addWidget(self.loadout_name_edit, stretch=1) right_layout.addLayout(name_layout) # Weapon configuration weapon_layout = QFormLayout(self.weapon_group) weapon_select_layout = QHBoxLayout() weapon_select_layout.addWidget(self.select_weapon_btn) weapon_select_layout.addWidget(self.weapon_name_label, stretch=1) weapon_layout.addRow("Weapon:", weapon_select_layout) weapon_layout.addRow("Damage:", self.weapon_damage_edit) weapon_layout.addRow("Decay/shot (PEC):", self.weapon_decay_edit) weapon_layout.addRow("Ammo/shot (PEC):", self.weapon_ammo_edit) weapon_layout.addRow("Total DPP:", self.dpp_label) # Attachments attachments_frame = QFrame() attachments_layout = QGridLayout(attachments_frame) attachments_layout.addWidget(QLabel("Amplifier:"), 0, 0) attachments_layout.addWidget(self.amp_label, 0, 1) self.attach_amp_btn.setText("šŸ” Search Amps") self.attach_amp_btn.clicked.connect(lambda: self._on_select_attachment("amplifier")) attachments_layout.addWidget(self.attach_amp_btn, 0, 2) attachments_layout.addWidget(self.remove_amp_btn, 0, 3) attachments_layout.addWidget(QLabel("Scope:"), 1, 0) attachments_layout.addWidget(self.scope_label, 1, 1) self.attach_scope_btn.setText("šŸ” Search Scopes") self.attach_scope_btn.clicked.connect(lambda: self._on_select_attachment("scope")) attachments_layout.addWidget(self.attach_scope_btn, 1, 2) attachments_layout.addWidget(self.remove_scope_btn, 1, 3) attachments_layout.addWidget(QLabel("Absorber:"), 2, 0) attachments_layout.addWidget(self.absorber_label, 2, 1) self.attach_absorber_btn.setText("šŸ” Search Absorbers") self.attach_absorber_btn.clicked.connect(lambda: self._on_select_attachment("absorber")) attachments_layout.addWidget(self.attach_absorber_btn, 2, 2) attachments_layout.addWidget(self.remove_absorber_btn, 2, 3) # Add enhancer selection button self.select_enhancer_btn = QPushButton("✨ Select Enhancers") self.select_enhancer_btn.setObjectName("selectButton") self.select_enhancer_btn.clicked.connect(self._on_select_enhancer) attachments_layout.addWidget(self.select_enhancer_btn, 3, 0, 1, 4) weapon_layout.addRow("Attachments:", attachments_frame) right_layout.addWidget(self.weapon_group) # Armor configuration - COMPLETE SYSTEM armor_layout = QVBoxLayout(self.armor_group) # Armor set selection row set_layout = QHBoxLayout() set_layout.addWidget(QLabel("Armor Set:")) set_layout.addWidget(self.armor_set_combo, stretch=1) set_layout.addWidget(self.equip_set_btn) set_layout.addWidget(self.clear_armor_btn) armor_layout.addLayout(set_layout) # Add API armor selector button self.select_armor_api_btn = QPushButton("šŸ” Search Entropia Nexus Armors") self.select_armor_api_btn.setObjectName("selectButton") self.select_armor_api_btn.clicked.connect(self._on_select_armor_from_api) armor_layout.addWidget(self.select_armor_api_btn) # Armor summary armor_layout.addWidget(self.armor_summary_label) # Separator separator = QFrame() separator.setFrameShape(QFrame.Shape.HLine) separator.setStyleSheet("background-color: #3d3d3d;") separator.setFixedHeight(2) armor_layout.addWidget(separator) # Individual slot widgets slots_label = QLabel("Individual Pieces & Plates:") slots_label.setStyleSheet("padding-top: 10px;") armor_layout.addWidget(slots_label) for slot in ALL_ARMOR_SLOTS: armor_layout.addWidget(self.slot_widgets[slot]) right_layout.addWidget(self.armor_group) # Healing configuration heal_layout = QFormLayout(self.heal_group) # Add healing tool search button self.select_healing_api_btn = QPushButton("šŸ” Search Healing Tools from Nexus") self.select_healing_api_btn.setObjectName("selectButton") self.select_healing_api_btn.clicked.connect(self._on_select_healing_from_api) heal_layout.addRow(self.select_healing_api_btn) heal_layout.addRow("Healing Tool:", self.heal_combo) heal_layout.addRow("Cost/heal (PEC):", self.heal_cost_edit) heal_layout.addRow("Heal amount:", self.heal_amount_edit) right_layout.addWidget(self.heal_group) # Accessories section (Rings, Clothing, Pets) self.accessories_group = DarkGroupBox("šŸ’ Accessories") accessories_layout = QVBoxLayout(self.accessories_group) # Rings - Left and Right rings_layout = QHBoxLayout() left_ring_layout = QVBoxLayout() left_ring_layout.addWidget(QLabel("Left Ring:")) self.left_ring_label = QLabel("None") self.left_ring_label.setStyleSheet("color: #888888;") left_ring_layout.addWidget(self.left_ring_label) self.select_left_ring_btn = QPushButton("šŸ” Select") self.select_left_ring_btn.setObjectName("selectButton") self.select_left_ring_btn.clicked.connect(self._on_select_left_ring) left_ring_layout.addWidget(self.select_left_ring_btn) rings_layout.addLayout(left_ring_layout) right_ring_layout = QVBoxLayout() right_ring_layout.addWidget(QLabel("Right Ring:")) self.right_ring_label = QLabel("None") self.right_ring_label.setStyleSheet("color: #888888;") right_ring_layout.addWidget(self.right_ring_label) self.select_right_ring_btn = QPushButton("šŸ” Select") self.select_right_ring_btn.setObjectName("selectButton") self.select_right_ring_btn.clicked.connect(self._on_select_right_ring) right_ring_layout.addWidget(self.select_right_ring_btn) rings_layout.addLayout(right_ring_layout) accessories_layout.addLayout(rings_layout) # Clothing and Pets other_accessories_layout = QHBoxLayout() self.select_clothing_btn = QPushButton("šŸ‘• Clothing") self.select_clothing_btn.setObjectName("selectButton") self.select_clothing_btn.clicked.connect(self._on_select_clothing) other_accessories_layout.addWidget(self.select_clothing_btn) self.select_pet_btn = QPushButton("🐾 Pet") self.select_pet_btn.setObjectName("selectButton") self.select_pet_btn.clicked.connect(self._on_select_pet) other_accessories_layout.addWidget(self.select_pet_btn) accessories_layout.addLayout(other_accessories_layout) right_layout.addWidget(self.accessories_group) right_layout.addWidget(self.accessories_group) # Cost summary - refined layout with better metrics summary_layout = QFormLayout(self.summary_group) # Weapon section weapon_group = QLabel("āš”ļø Weapon") weapon_group.setStyleSheet("color: #e0e0e0; margin-top: 5px;") summary_layout.addRow(weapon_group) summary_layout.addRow(" Cost/Shot:", self.cost_per_shot_label) summary_layout.addRow(" DPS:", self.dps_label) summary_layout.addRow(" DPP:", self.dpp_label) # Armor section armor_group = QLabel("šŸ›”ļø Armor") armor_group.setStyleSheet("color: #e0e0e0; margin-top: 5px;") summary_layout.addRow(armor_group) summary_layout.addRow(" Cost/Hit:", self.cost_per_hit_label) summary_layout.addRow(" Protection:", self.protection_summary_label) # Healing section heal_group = QLabel("šŸ’š Healing") heal_group.setStyleSheet("color: #e0e0e0; margin-top: 5px;") summary_layout.addRow(heal_group) summary_layout.addRow(" Cost/Heal:", self.cost_per_heal_label) summary_layout.addRow(" HP/PEC:", self.hp_per_pec_label) # Break-even calculator break_even_group = QLabel("šŸ“ˆ Break-Even") break_even_group.setStyleSheet("color: #e0e0e0; margin-top: 10px;") summary_layout.addRow(break_even_group) break_even_layout = QHBoxLayout() break_even_layout.addWidget(QLabel("Mob HP:")) break_even_layout.addWidget(self.mob_health_edit) break_even_layout.addWidget(self.calc_break_even_btn) summary_layout.addRow(" Calculate:", break_even_layout) summary_layout.addRow(" Break-even:", self.break_even_label) right_layout.addWidget(self.summary_group) # Save button right_layout.addWidget(self.save_btn) right_layout.addStretch() right_scroll.setWidget(right_widget) # Splitter splitter = QSplitter(Qt.Orientation.Horizontal) splitter.addWidget(left_panel) splitter.addWidget(right_scroll) splitter.setSizes([250, 850]) main_layout.addWidget(splitter) def _connect_signals(self): """Connect all signal handlers.""" # Weapon selection self.select_weapon_btn.clicked.connect(self._on_select_weapon) self.weapon_damage_edit.textChanged.connect(self._update_calculations) self.weapon_decay_edit.textChanged.connect(self._update_calculations) self.weapon_ammo_edit.textChanged.connect(self._update_calculations) # Attachments self.attach_amp_btn.clicked.connect(lambda: self._on_attach("amplifier")) self.attach_scope_btn.clicked.connect(lambda: self._on_attach("scope")) self.attach_absorber_btn.clicked.connect(lambda: self._on_attach("absorber")) self.remove_amp_btn.clicked.connect(self._on_remove_amp) self.remove_scope_btn.clicked.connect(self._on_remove_scope) self.remove_absorber_btn.clicked.connect(self._on_remove_absorber) # Armor self.equip_set_btn.clicked.connect(self._on_equip_full_set) self.clear_armor_btn.clicked.connect(self._on_clear_armor) # Healing self.heal_combo.currentTextChanged.connect(self._on_heal_changed) # Buttons self.save_btn.clicked.connect(self._save_loadout) self.load_btn.clicked.connect(self._load_selected) self.delete_btn.clicked.connect(self._delete_selected) self.new_btn.clicked.connect(self._new_loadout) self.refresh_btn.clicked.connect(self._load_saved_loadouts) self.close_btn.clicked.connect(self.reject) self.calc_break_even_btn.clicked.connect(self._calculate_break_even) # Double click on list self.saved_list.itemDoubleClicked.connect(self._load_from_item) def _populate_armor_sets(self): """Populate armor set combo.""" self.armor_set_combo.clear() self.armor_set_combo.addItem("-- Select a Set --") sets = get_all_armor_sets() for armor_set in sets: total_prot = armor_set.get_total_protection().get_total() display = f"{armor_set.name} (Prot: {total_prot})" self.armor_set_combo.addItem(display, armor_set) def _populate_healing_data(self): """Populate healing combo with real data from database.""" self.heal_combo.clear() self.heal_combo.addItem("-- Custom --") # Get real healing tools healing_tools = get_healing_tools_data() # Sort by category (chips last) medical_tools = [h for h in healing_tools if not h.get("is_chip", False)] chips = [h for h in healing_tools if h.get("is_chip", False)] # Add medical tools first if medical_tools: self.heal_combo.addItem("--- Medical Tools ---") for tool in medical_tools: self.heal_combo.addItem(tool["name"]) # Add restoration chips if chips: self.heal_combo.addItem("--- Restoration Chips ---") for chip in sorted(chips, key=lambda x: x["amount"]): self.heal_combo.addItem(chip["name"]) def _on_select_weapon(self): """Open weapon selector dialog.""" dialog = WeaponSelectorDialog(self) dialog.weapon_selected.connect(self._on_weapon_selected) dialog.exec() def _on_weapon_selected(self, weapon: WeaponStats): """Handle weapon selection.""" self.current_weapon = weapon self.weapon_name_label.setText(weapon.name) self.weapon_damage_edit.set_decimal(weapon.total_damage) self.weapon_decay_edit.set_decimal(weapon.decay or Decimal("0")) self.weapon_ammo_edit.set_decimal(Decimal(weapon.ammo_burn or 0)) self._update_calculations() def _on_select_armor_from_api(self): """Open armor selector dialog from Nexus API.""" from ui.armor_selector import ArmorSelectorDialog dialog = ArmorSelectorDialog(self) dialog.armor_selected.connect(self._on_api_armor_selected) dialog.armor_set_selected.connect(self._on_api_armor_set_selected) dialog.exec() def _on_api_armor_selected(self, armor: NexusArmor): """Handle individual armor piece selection from API.""" # Store selected armor info self._selected_api_armor = armor QMessageBox.information( self, "Armor Selected", f"Selected: {armor.name}\n" f"Type: {armor.type}\n" f"Durability: {armor.durability}\n" f"Protection: Impact {armor.protection_impact}, Cut {armor.protection_cut}, Stab {armor.protection_stab}" ) def _on_api_armor_set_selected(self, armor_set: 'NexusArmorSet'): """Handle full armor set selection from API.""" from core.nexus_full_api import get_nexus_api # Get all armors to find matching pieces with full data api = get_nexus_api() all_armors = api.get_all_armors() # Map slot names to armor slot widgets slot_mapping = { 'head': self.slot_widgets[ArmorSlot.HEAD], 'torso': self.slot_widgets[ArmorSlot.TORSO], 'harness': self.slot_widgets[ArmorSlot.TORSO], 'chest': self.slot_widgets[ArmorSlot.TORSO], 'arms': self.slot_widgets[ArmorSlot.ARMS], 'arm guards': self.slot_widgets[ArmorSlot.ARMS], 'armguards': self.slot_widgets[ArmorSlot.ARMS], 'hands': self.slot_widgets[ArmorSlot.HANDS], 'gloves': self.slot_widgets[ArmorSlot.HANDS], 'legs': self.slot_widgets[ArmorSlot.LEGS], 'thigh guards': self.slot_widgets[ArmorSlot.LEGS], 'thighguards': self.slot_widgets[ArmorSlot.LEGS], 'shins': self.slot_widgets[ArmorSlot.SHINS], 'shin guards': self.slot_widgets[ArmorSlot.SHINS], 'shinguards': self.slot_widgets[ArmorSlot.SHINS], 'feet': self.slot_widgets[ArmorSlot.FEET], 'foot guards': self.slot_widgets[ArmorSlot.FEET], 'footguards': self.slot_widgets[ArmorSlot.FEET], } pieces_found = 0 pieces_not_found = [] # Store the full set for protection calculations self.current_armor_set = armor_set # Try to find each piece in the set for piece_name in armor_set.pieces: # Find the armor in the API results matching_armor = None for armor in all_armors: if armor.name == piece_name: matching_armor = armor break if matching_armor: # Get slot from the armor's Type field or parse from name armor_type = matching_armor.type.lower() slot_widget = None # First try direct Type match if armor_type in slot_mapping: slot_widget = slot_mapping[armor_type] else: # Try parsing from armor type for type_key, widget in slot_mapping.items(): if type_key in armor_type: slot_widget = widget break # If still no match, try parsing from name if not slot_widget: name_lower = piece_name.lower() if 'helmet' in name_lower or 'cap' in name_lower: slot_widget = self.slot_widgets[ArmorSlot.HEAD] elif 'harness' in name_lower or 'chest' in name_lower or 'torso' in name_lower: slot_widget = self.slot_widgets[ArmorSlot.TORSO] elif 'arm' in name_lower or 'shoulder' in name_lower: slot_widget = self.slot_widgets[ArmorSlot.ARMS] elif 'glove' in name_lower or 'hand' in name_lower: slot_widget = self.slot_widgets[ArmorSlot.HANDS] elif 'thigh' in name_lower or ('leg' in name_lower and 'shin' not in name_lower): slot_widget = self.slot_widgets[ArmorSlot.LEGS] elif 'shin' in name_lower: slot_widget = self.slot_widgets[ArmorSlot.SHINS] elif 'foot' in name_lower or 'boot' in name_lower: slot_widget = self.slot_widgets[ArmorSlot.FEET] if slot_widget: # Create ArmorPiece from NexusArmor # Use the SET'S protection values, not individual piece (which are 0 in API) from core.armor_system import ArmorPiece piece = ArmorPiece( item_id=matching_armor.item_id, name=matching_armor.name, slot=self._get_slot_from_type(armor_type) if armor_type in slot_mapping else self._get_slot_from_name(piece_name), # Use armor set's total protection for each piece # In EU, protection comes from the full set, not individual pieces protection=ProtectionProfile( impact=armor_set.total_protection.impact, cut=armor_set.total_protection.cut, stab=armor_set.total_protection.stab, burn=armor_set.total_protection.burn, cold=armor_set.total_protection.cold, acid=armor_set.total_protection.acid, electric=armor_set.total_protection.electric, ), durability=matching_armor.durability, decay_per_hp=Decimal("0.05") * (Decimal(1) - Decimal(matching_armor.durability) / Decimal("100000")) ) slot_widget.set_piece(piece) pieces_found += 1 else: pieces_not_found.append(f"{piece_name} (unknown slot: {armor_type})") else: pieces_not_found.append(piece_name) # Show summary msg = f"Equipped armor set: {armor_set.name}\n\n" msg += f"āœ“ Found and equipped {pieces_found}/{len(armor_set.pieces)} pieces\n" if pieces_not_found: msg += f"\n⚠ Not found:\n" + "\n".join([f" • {p}" for p in pieces_not_found]) if armor_set.set_bonus: msg += f"\n\n✨ Set Bonus: {armor_set.set_bonus}" QMessageBox.information(self, "Armor Set Equipped", msg) self._update_calculations() def _get_slot_from_type(self, armor_type: str) -> 'ArmorSlot': """Map armor type string to ArmorSlot enum.""" armor_type = armor_type.lower() if 'head' in armor_type or 'helmet' in armor_type: return ArmorSlot.HEAD elif 'torso' in armor_type or 'harness' in armor_type or 'chest' in armor_type: return ArmorSlot.TORSO elif 'arm' in armor_type: return ArmorSlot.ARMS elif 'hand' in armor_type or 'glove' in armor_type: return ArmorSlot.HANDS elif 'thigh' in armor_type or ('leg' in armor_type and 'shin' not in armor_type): return ArmorSlot.LEGS elif 'shin' in armor_type: return ArmorSlot.SHINS elif 'foot' in armor_type or 'boot' in armor_type: return ArmorSlot.FEET else: return ArmorSlot.TORSO # Default def _get_slot_from_name(self, piece_name: str) -> 'ArmorSlot': """Map armor piece name to ArmorSlot enum.""" name_lower = piece_name.lower() if 'helmet' in name_lower or 'cap' in name_lower: return ArmorSlot.HEAD elif 'harness' in name_lower or 'chest' in name_lower or 'torso' in name_lower: return ArmorSlot.TORSO elif 'arm' in name_lower or 'shoulder' in name_lower: return ArmorSlot.ARMS elif 'glove' in name_lower or 'hand' in name_lower: return ArmorSlot.HANDS elif 'thigh' in name_lower or ('leg' in name_lower and 'shin' not in name_lower): return ArmorSlot.LEGS elif 'shin' in name_lower: return ArmorSlot.SHINS elif 'foot' in name_lower or 'boot' in name_lower: return ArmorSlot.FEET else: return ArmorSlot.TORSO # Default def _on_select_healing_from_api(self): """Open healing tool selector dialog from Nexus API.""" from ui.healing_selector import HealingSelectorDialog dialog = HealingSelectorDialog(self) dialog.tool_selected.connect(self._on_api_healing_selected) dialog.exec() def _on_api_healing_selected(self, tool: NexusHealingTool): """Handle healing tool selection from API.""" self._selected_api_healing = tool # Update the healing combo to show selected tool # Find or add the tool to combo index = self.heal_combo.findText(tool.name) if index < 0: self.heal_combo.addItem(tool.name) index = self.heal_combo.count() - 1 self.heal_combo.setCurrentIndex(index) # Update cost and amount fields self.heal_cost_edit.setText(str(tool.decay)) self.heal_amount_edit.setText(str(tool.heal_amount)) self._update_calculations() QMessageBox.information( self, "Healing Tool Selected", f"Selected: {tool.name}\n" f"Heal: {tool.heal_amount} HP\n" f"Decay: {tool.decay:.2f} PEC ({tool.heal_per_pec:.2f} hp/pec)" ) def _on_select_attachment(self, attachment_type: str): """Open attachment selector dialog from Nexus API.""" from ui.attachment_selector import AttachmentSelectorDialog dialog = AttachmentSelectorDialog(self, attachment_type=attachment_type) dialog.attachment_selected.connect( lambda att: self._on_api_attachment_selected(att, attachment_type) ) dialog.exec() def _on_api_attachment_selected(self, attachment: NexusAttachment, att_type: str): """Handle attachment selection from API.""" # Update UI based on attachment type if att_type == "amplifier": self.amp_label.setText(f"{attachment.name} (+{attachment.damage_bonus} dmg)") elif att_type == "scope": self.scope_label.setText(f"{attachment.name} (+{attachment.range_bonus} rng)") elif att_type == "absorber": self.absorber_label.setText(f"{attachment.name}") QMessageBox.information( self, "Attachment Selected", f"Selected: {attachment.name}\n" f"Type: {attachment.attachment_type.title()}\n" f"Damage: +{attachment.damage_bonus}\n" f"Range: +{attachment.range_bonus}\n" f"Decay: {attachment.decay:.2f} PEC" ) def _on_select_enhancer(self): """Open enhancer selector dialog.""" from ui.enhancer_selector import EnhancerSelectorDialog dialog = EnhancerSelectorDialog(self) dialog.enhancer_selected.connect(self._on_api_enhancer_selected) dialog.exec() def _on_api_enhancer_selected(self, enhancer: NexusEnhancer): """Handle enhancer selection from API.""" QMessageBox.information( self, "Enhancer Selected", f"Selected: {enhancer.name}\n" f"Type: {enhancer.enhancer_type.title()}\n" f"Tier: {enhancer.tier}\n" f"Effect: +{enhancer.effect_value}%\n" f"Break Chance: {enhancer.break_chance * 100:.1f}%" ) def _on_select_left_ring(self): """Open ring selector for left finger.""" from ui.accessories_selector import AccessoriesSelectorDialog dialog = AccessoriesSelectorDialog(self, slot_filter="Left Finger") dialog.ring_selected.connect(self._on_left_ring_selected) dialog.exec() def _on_select_right_ring(self): """Open ring selector for right finger.""" from ui.accessories_selector import AccessoriesSelectorDialog dialog = AccessoriesSelectorDialog(self, slot_filter="Right Finger") dialog.ring_selected.connect(self._on_right_ring_selected) dialog.exec() def _on_left_ring_selected(self, ring: NexusRing): """Handle left ring selection.""" self.current_left_ring = ring effects_str = ", ".join([f"{k}: {v}" for k, v in ring.effects.items()]) if ring.effects else "No effects" self.left_ring_label.setText(f"{ring.name}\n{effects_str}") self.left_ring_label.setStyleSheet("color: #4caf50;") def _on_right_ring_selected(self, ring: NexusRing): """Handle right ring selection.""" self.current_right_ring = ring effects_str = ", ".join([f"{k}: {v}" for k, v in ring.effects.items()]) if ring.effects else "No effects" self.right_ring_label.setText(f"{ring.name}\n{effects_str}") self.right_ring_label.setStyleSheet("color: #4caf50;") def _on_select_clothing(self): """Open clothing selector.""" from ui.accessories_selector import AccessoriesSelectorDialog dialog = AccessoriesSelectorDialog(self, initial_tab="clothing") dialog.clothing_selected.connect(self._on_clothing_selected) dialog.exec() def _on_select_pet(self): """Open pet selector.""" from ui.accessories_selector import AccessoriesSelectorDialog dialog = AccessoriesSelectorDialog(self, initial_tab="pets") dialog.pet_selected.connect(self._on_pet_selected) dialog.exec() def _on_select_accessories(self): """Open accessories selector dialog (rings, clothing, pets) - legacy.""" self._on_select_left_ring() def _on_ring_selected(self, ring: NexusRing): """Handle ring selection - legacy, routes to appropriate slot.""" if ring.slot == "Left Finger": self._on_left_ring_selected(ring) else: self._on_right_ring_selected(ring) def _on_clothing_selected(self, clothing: NexusClothing): """Handle clothing selection.""" buffs = ", ".join([f"{k}:{v}" for k, v in clothing.buffs.items()]) QMessageBox.information( self, "Clothing Selected", f"Selected: {clothing.name}\n" f"Slot: {clothing.slot}\n" f"Buffs: {buffs if buffs else 'None'}" ) def _on_pet_selected(self, pet: NexusPet): """Handle pet selection.""" QMessageBox.information( self, "Pet Selected", f"Selected: {pet.name}\n" f"Effect: {pet.effect_type} {pet.effect_value}\n" f"Level Required: {pet.level_required if pet.level_required > 0 else 'None'}" ) def _on_attach(self, attachment_type: str): """Handle attachment selection (legacy - now uses API).""" self._on_select_attachment(attachment_type) def _apply_attachment(self, attachment_type: str, att): """Apply selected attachment.""" if attachment_type == "amplifier": self.amp_label.setText(f"{att.name} (+{att.damage_increase} dmg)") elif attachment_type == "scope": self.scope_label.setText(f"{att.name} (+{att.range_increase}m)") elif attachment_type == "absorber": self.absorber_label.setText(f"{att.name} (-{att.damage_reduction} dmg)") self._update_calculations() def _clear_attachment(self, attachment_type: str): """Clear an attachment.""" if attachment_type == "amplifier": self.amp_label.setText("None") elif attachment_type == "scope": self.scope_label.setText("None") elif attachment_type == "absorber": self.absorber_label.setText("None") self._update_calculations() def _on_remove_amp(self): """Remove amplifier.""" self.amp_label.setText("None") self._update_calculations() def _on_remove_scope(self): """Remove scope.""" self.scope_label.setText("None") self._update_calculations() def _on_remove_absorber(self): """Remove absorber.""" self.absorber_label.setText("None") self._update_calculations() def _on_equip_full_set(self): """Equip a full armor set.""" if self.armor_set_combo.currentIndex() <= 0: QMessageBox.information(self, "No Selection", "Please select an armor set first.") return armor_set = self.armor_set_combo.currentData() if not armor_set: return # Clear any individual pieces for widget in self.slot_widgets.values(): widget.set_piece(None) widget.set_plate(None) # Equip set pieces for slot, piece in armor_set.pieces.items(): if slot in self.slot_widgets: self.slot_widgets[slot].set_piece(piece) self.current_armor_set = armor_set self._update_armor_summary() self._update_calculations() QMessageBox.information(self, "Set Equipped", f"Equipped {armor_set.name}") def _on_clear_armor(self): """Clear all armor.""" for widget in self.slot_widgets.values(): widget.set_piece(None) widget.set_plate(None) self.current_armor_set = None self.armor_set_combo.setCurrentIndex(0) self._update_armor_summary() self._update_calculations() def _on_armor_changed(self): """Handle armor piece or plate change.""" # If individual pieces are changed, we're no longer using a pure full set if self.current_armor_set: # Check if all pieces match the set all_match = True for slot, piece in self.current_armor_set.pieces.items(): widget = self.slot_widgets.get(slot) if widget: current = widget.get_piece() if not current or current.item_id != piece.item_id: all_match = False break if not all_match: self.current_armor_set = None self._update_armor_summary() self._update_calculations() def _update_armor_summary(self): """Update armor summary display.""" equipped_count = 0 for widget in self.slot_widgets.values(): if widget.get_piece(): equipped_count += 1 if equipped_count == 0: self.armor_summary_label.setText("No armor equipped") self.armor_summary_label.setStyleSheet("color: #888888; padding: 5px;") elif equipped_count == 7: if self.current_armor_set: self.armor_summary_label.setText(f"āœ“ Full Set: {self.current_armor_set.name}") self.armor_summary_label.setStyleSheet("color: #4caf50; font-weight: bold; padding: 5px;") else: self.armor_summary_label.setText(f"āœ“ 7/7 pieces equipped (Mixed Set)") self.armor_summary_label.setStyleSheet("color: #4caf50; padding: 5px;") else: self.armor_summary_label.setText(f"⚠ {equipped_count}/7 pieces equipped") self.armor_summary_label.setStyleSheet("color: #ff9800; padding: 5px;") def _on_heal_changed(self, name: str): """Handle healing selection change.""" if name == "-- Custom --": self.heal_cost_edit.setEnabled(True) self.heal_amount_edit.setEnabled(True) self.heal_cost_edit.clear() self.heal_amount_edit.clear() else: for heal in MOCK_HEALING: if heal["name"] == name: self.heal_cost_edit.set_decimal(heal["cost"]) self.heal_amount_edit.set_decimal(heal["amount"]) break self.heal_cost_edit.setEnabled(False) self.heal_amount_edit.setEnabled(False) self._update_calculations() def _update_calculations(self): """Update all cost and performance calculations.""" try: config = self._get_current_config() # Weapon metrics (per shot, not per hour) cost_per_shot_pec = config.get_total_decay_per_shot() + config.get_total_ammo_per_shot() cost_per_shot_ped = cost_per_shot_pec / Decimal("100") # Convert PEC to PED self.cost_per_shot_label.setText(f"{cost_per_shot_ped:.4f} PED") # DPS calculation dps = config.calculate_dps() self.dps_label.setText(f"{dps:.2f}") # DPP (Damage Per Pec) dpp = config.calculate_dpp() self.dpp_label.setText(f"{dpp:.4f}") # Armor metrics (cost per hit) cost_per_hit = config.get_armor_decay_per_hit() # Already returns PED self.cost_per_hit_label.setText(f"{cost_per_hit:.4f} PED") # Protection summary protection = config.get_total_protection() prot_text = format_protection(protection) if prot_text == "None": self.protection_summary_label.setText("No protection") else: self.protection_summary_label.setText(f"Total: {protection.get_total():.1f} | {prot_text}") # Healing metrics cost_per_heal = config.get_heal_cost_per_use() # Already returns PED self.cost_per_heal_label.setText(f"{cost_per_heal:.4f} PED") hp_per_pec = config.get_hp_per_pec() self.hp_per_pec_label.setText(f"{hp_per_pec:.2f}") except Exception as e: logger.error(f"Calculation error: {e}") def _calculate_break_even(self): """Calculate and display break-even loot value.""" try: config = self._get_current_config() mob_health = self.mob_health_edit.get_decimal() if mob_health <= 0: QMessageBox.warning(self, "Invalid Input", "Mob health must be greater than 0") return break_even = config.calculate_break_even(mob_health) self.break_even_label.setText( f"Break-even: {break_even:.2f} PED (mob HP: {mob_health})" ) except Exception as e: QMessageBox.critical(self, "Error", f"Calculation failed: {str(e)}") def _get_current_config(self) -> LoadoutConfig: """Get current configuration from UI fields.""" # Build equipped armor from slot widgets equipped = EquippedArmor() for slot, widget in self.slot_widgets.items(): piece = widget.get_piece() if piece: # Create a copy piece_copy = ArmorPiece( name=piece.name, item_id=piece.item_id, slot=piece.slot, set_name=piece.set_name, decay_per_hp=piece.decay_per_hp, protection=ProtectionProfile( stab=piece.protection.stab, cut=piece.protection.cut, impact=piece.protection.impact, penetration=piece.protection.penetration, shrapnel=piece.protection.shrapnel, burn=piece.protection.burn, cold=piece.protection.cold, acid=piece.protection.acid, electric=piece.protection.electric, ), durability=piece.durability, weight=piece.weight, ) # Attach plate if selected plate = widget.get_plate() if plate: plate_copy = ArmorPlate( name=plate.name, item_id=plate.item_id, decay_per_hp=plate.decay_per_hp, protection=ProtectionProfile( stab=plate.protection.stab, cut=plate.protection.cut, impact=plate.protection.impact, penetration=plate.protection.penetration, shrapnel=plate.protection.shrapnel, burn=plate.protection.burn, cold=plate.protection.cold, acid=plate.protection.acid, electric=plate.protection.electric, ), durability=plate.durability, ) piece_copy.attach_plate(plate_copy) equipped.equip_piece(piece_copy) # Note: We don't call equip_full_set here because: # 1. current_armor_set is a NexusArmorSet (API type), not ArmorSet (core type) # 2. Individual pieces already have the full set protection values stored on them # 3. The EquippedArmor.get_total_protection() will sum up pieces correctly return LoadoutConfig( name=self.loadout_name_edit.text().strip() or "Unnamed", weapon_name=self.current_weapon.name if self.current_weapon else (self.weapon_name_label.text() if self.weapon_name_label.text() != "No weapon selected" else "-- Custom --"), weapon_id=self.current_weapon.id if self.current_weapon else 0, weapon_damage=self.weapon_damage_edit.get_decimal(), weapon_decay_pec=self.weapon_decay_edit.get_decimal(), weapon_ammo_pec=self.weapon_ammo_edit.get_decimal(), equipped_armor=equipped if equipped.get_all_pieces() else None, armor_set_name=self.current_armor_set.name if self.current_armor_set else "-- Mixed --", heal_name=self.heal_combo.currentText(), heal_cost_pec=self.heal_cost_edit.get_decimal(), heal_amount=self.heal_amount_edit.get_decimal(), shots_per_hour=3600, # Default, no longer in UI hits_per_hour=720, # Default, no longer in UI heals_per_hour=60, # Default, no longer in UI ) def _set_config(self, config: LoadoutConfig): """Set UI fields from configuration.""" self.loadout_name_edit.setText(config.name) # Weapon self.weapon_name_label.setText(config.weapon_name) self.weapon_damage_edit.set_decimal(config.weapon_damage) self.weapon_decay_edit.set_decimal(config.weapon_decay_pec) self.weapon_ammo_edit.set_decimal(config.weapon_ammo_pec) # Weapon attachments (simplified - just labels) self.amp_label.setText("None") self.scope_label.setText("None") self.absorber_label.setText("None") # Armor - use equipped_armor if available if config.equipped_armor: self.equipped_armor = config.equipped_armor pieces = config.equipped_armor.get_all_pieces() for slot, widget in self.slot_widgets.items(): piece = pieces.get(slot) widget.set_piece(piece) if piece and piece.attached_plate: widget.set_plate(piece.attached_plate) else: widget.set_plate(None) # Check if it's a full set if config.equipped_armor.full_set: self.current_armor_set = config.equipped_armor.full_set # Select in combo for i in range(self.armor_set_combo.count()): data = self.armor_set_combo.itemData(i) if data and data.set_id == self.current_armor_set.set_id: self.armor_set_combo.setCurrentIndex(i) break else: self.current_armor_set = None self.armor_set_combo.setCurrentIndex(0) else: # Legacy or empty self._on_clear_armor() self._update_armor_summary() # Healing self.heal_combo.setCurrentText(config.heal_name) self.heal_cost_edit.set_decimal(config.heal_cost_pec) self.heal_amount_edit.set_decimal(config.heal_amount) # Store config self.current_loadout = config self._update_calculations() def _save_loadout(self): """Save current loadout to file.""" name = self.loadout_name_edit.text().strip() if not name: QMessageBox.warning(self, "Missing Name", "Please enter a loadout name") return safe_name = "".join(c for c in name if c.isalnum() or c in "._- ").strip() if not safe_name: safe_name = "unnamed" config = self._get_current_config() config.name = name filepath = self.config_dir / f"{safe_name}.json" try: with open(filepath, 'w') as f: json.dump(config.to_dict(), f, indent=2) self.current_loadout = config self.loadout_saved.emit(config) self._load_saved_loadouts() QMessageBox.information(self, "Saved", f"Loadout '{name}' saved successfully!") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save: {str(e)}") def _load_saved_loadouts(self): """Load list of saved loadouts.""" self.saved_list.clear() try: for filepath in sorted(self.config_dir.glob("*.json")): try: with open(filepath, 'r') as f: data = json.load(f) config = LoadoutConfig.from_dict(data) item = QListWidgetItem(f"šŸ“‹ {config.name}") item.setData(Qt.ItemDataRole.UserRole, str(filepath)) # Build tooltip dpp = config.calculate_dpp() cost = config.calculate_total_cost_per_hour() tooltip = ( f"Weapon: {config.weapon_name}\n" f"Armor: {config.armor_set_name}\n" f"Total DPP: {dpp:.3f}\n" f"Cost/hr: {cost:.2f} PED" ) item.setToolTip(tooltip) self.saved_list.addItem(item) except Exception as e: logger.error(f"Failed to load {filepath}: {e}") continue except Exception as e: logger.error(f"Failed to list loadouts: {e}") def _load_selected(self): """Load the selected loadout from the list.""" item = self.saved_list.currentItem() if item: self._load_from_item(item) else: QMessageBox.information(self, "No Selection", "Please select a loadout to load") def _load_from_item(self, item: QListWidgetItem): """Load loadout from a list item.""" filepath = item.data(Qt.ItemDataRole.UserRole) if not filepath: return try: with open(filepath, 'r') as f: data = json.load(f) config = LoadoutConfig.from_dict(data) self._set_config(config) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}") def _delete_selected(self): """Delete the selected loadout.""" item = self.saved_list.currentItem() if not item: QMessageBox.information(self, "No Selection", "Please select a loadout to delete") return filepath = item.data(Qt.ItemDataRole.UserRole) name = item.text().replace("šŸ“‹ ", "") reply = QMessageBox.question( self, "Confirm Delete", f"Are you sure you want to delete '{name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: os.remove(filepath) self._load_saved_loadouts() QMessageBox.information(self, "Deleted", f"'{name}' deleted successfully") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to delete: {str(e)}") def _new_loadout(self): """Clear all fields for a new loadout.""" self.loadout_name_edit.clear() self.weapon_name_label.setText("No weapon selected") # Clear weapon self.weapon_damage_edit.clear() self.weapon_decay_edit.clear() self.weapon_ammo_edit.clear() # Clear attachments self.amp_label.setText("None") self.scope_label.setText("None") self.absorber_label.setText("None") # Clear armor self._on_clear_armor() # Clear healing self.heal_cost_edit.clear() self.heal_amount_edit.clear() # Reset values self.mob_health_edit.set_decimal(Decimal("100")) # Reset combos self.heal_combo.setCurrentIndex(0) # Clear stored objects self.current_weapon = None self.current_armor_set = None self.current_loadout = None self._update_calculations() def get_current_loadout(self) -> Optional[LoadoutConfig]: """Get the currently loaded/created loadout.""" return self.current_loadout # ============================================================================ # Main entry point for testing # ============================================================================ def main(): """Run the loadout manager as a standalone application.""" import sys # Setup logging logging.basicConfig(level=logging.INFO) app = QApplication(sys.argv) app.setStyle('Fusion') # Set application-wide font font = QFont("Segoe UI", 10) app.setFont(font) dialog = LoadoutManagerDialog() # Connect signal for testing dialog.loadout_saved.connect(lambda cfg: print(f"Loadout saved: {cfg.name}")) if dialog.exec() == QDialog.DialogCode.Accepted: config = dialog.get_current_loadout() if config: print(f"\nFinal Loadout: {config.name}") print(f" Weapon: {config.weapon_name}") print(f" Armor: {config.armor_set_name}") if config.equipped_armor: pieces = config.equipped_armor.get_all_pieces() print(f" Armor Pieces: {len(pieces)}/7") print(f" Total DPP: {config.calculate_dpp():.4f}") print(f" Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr") print(f" Protection: {format_protection(config.get_total_protection())}") sys.exit(0) if __name__ == "__main__": main()