""" 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.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 # Armor System equipped_armor: Optional[EquippedArmor] = None armor_set_name: str = "-- None --" # 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") # 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.""" 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 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_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 using the equipped armor system.""" if self.equipped_armor: return self.equipped_armor.get_total_decay_per_hit() * Decimal(self.hits_per_hour) # Legacy fallback return self.armor_decay_pec * Decimal(self.hits_per_hour) 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 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 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) # ============================================================================ # Mock Data for Healing # ============================================================================ MOCK_HEALING = [ {"name": "Vivo T10", "cost": Decimal("2.0"), "amount": Decimal("12")}, {"name": "Vivo T15", "cost": Decimal("3.5"), "amount": Decimal("18")}, {"name": "Vivo S10", "cost": Decimal("4.0"), "amount": Decimal("25")}, {"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5"), "amount": Decimal("8")}, {"name": "Restoration Chip I", "cost": Decimal("5.0"), "amount": Decimal("30")}, {"name": "Restoration Chip II", "cost": Decimal("8.0"), "amount": Decimal("50")}, {"name": "Restoration Chip III", "cost": Decimal("12.0"), "amount": Decimal("80")}, {"name": "Mod 2350", "cost": Decimal("15.0"), "amount": Decimal("100")}, ] # ============================================================================ # 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) # 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.""" names = { ArmorSlot.HEAD: "Head", ArmorSlot.CHEST: "Chest", ArmorSlot.LEFT_ARM: "Left Arm", ArmorSlot.RIGHT_ARM: "Right Arm", ArmorSlot.LEFT_HAND: "Left Hand", ArmorSlot.RIGHT_HAND: "Right Hand", ArmorSlot.LEGS: "Legs/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_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) return # Find and select the piece for i in range(self.piece_combo.count()): data = self.piece_combo.itemData(i) if data and data.item_id == piece.item_id: self.piece_combo.setCurrentIndex(i) return self.piece_combo.setCurrentIndex(0) 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.""" decay = Decimal("0") if self.current_piece: decay += self.current_piece.decay_per_hit if self.current_plate: decay += self.current_plate.decay_per_hit 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() # ============================================================================ # Main Loadout Manager Dialog # ============================================================================ class LoadoutManagerDialog(QDialog): """Main dialog for managing hunting loadouts with full armor system.""" loadout_saved = pyqtSignal(str) 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 # 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...") # Activity settings self.shots_per_hour_spin = QSpinBox() self.shots_per_hour_spin.setRange(1, 20000) self.shots_per_hour_spin.setValue(3600) self.shots_per_hour_spin.setSuffix(" /hr") self.hits_per_hour_spin = QSpinBox() self.hits_per_hour_spin.setRange(0, 5000) self.hits_per_hour_spin.setValue(720) self.hits_per_hour_spin.setSuffix(" /hr") self.heals_per_hour_spin = QSpinBox() self.heals_per_hour_spin.setRange(0, 500) self.heals_per_hour_spin.setValue(60) self.heals_per_hour_spin.setSuffix(" /hr") # 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 self.summary_group = DarkGroupBox("📊 Cost Summary") self.weapon_cost_label = QLabel("0.00 PEC/hr") self.armor_cost_label = QLabel("0.00 PEC/hr") self.heal_cost_label = QLabel("0.00 PEC/hr") self.total_cost_label = QLabel("0.00 PED/hr") self.total_cost_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 18px;") self.total_dpp_label = QLabel("0.0000") self.total_dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 18px;") # Protection summary self.protection_summary_label = QLabel("No protection") self.protection_summary_label.setStyleSheet("color: #4a90d9; font-size: 12px;") # 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) # Activity settings activity_group = DarkGroupBox("⚙️ Activity Settings") activity_layout = QGridLayout(activity_group) activity_layout.addWidget(QLabel("Shots/Hour:"), 0, 0) activity_layout.addWidget(self.shots_per_hour_spin, 0, 1) activity_layout.addWidget(QLabel("Hits Taken/Hour:"), 0, 2) activity_layout.addWidget(self.hits_per_hour_spin, 0, 3) activity_layout.addWidget(QLabel("Heals/Hour:"), 0, 4) activity_layout.addWidget(self.heals_per_hour_spin, 0, 5) right_layout.addWidget(activity_group) # 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) 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) 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) attachments_layout.addWidget(self.attach_absorber_btn, 2, 2) attachments_layout.addWidget(self.remove_absorber_btn, 2, 3) 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) # 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) 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) # Cost summary summary_layout = QFormLayout(self.summary_group) summary_layout.addRow("Weapon Cost:", self.weapon_cost_label) summary_layout.addRow("Armor Cost:", self.armor_cost_label) summary_layout.addRow("Healing Cost:", self.heal_cost_label) summary_layout.addRow("Total DPP:", self.total_dpp_label) summary_layout.addRow("Total Cost:", self.total_cost_label) # Protection summary summary_layout.addRow("Protection:", self.protection_summary_label) break_even_layout = QHBoxLayout() break_even_layout.addWidget(QLabel("Mob Health:")) break_even_layout.addWidget(self.mob_health_edit) break_even_layout.addWidget(self.calc_break_even_btn) summary_layout.addRow("Break-Even:", break_even_layout) summary_layout.addRow("", 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) # Activity settings self.shots_per_hour_spin.valueChanged.connect(self._update_calculations) self.hits_per_hour_spin.valueChanged.connect(self._update_calculations) self.heals_per_hour_spin.valueChanged.connect(self._update_calculations) # 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 data.""" self.heal_combo.clear() self.heal_combo.addItem("-- Custom --") for heal in MOCK_HEALING: self.heal_combo.addItem(heal["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_attach(self, attachment_type: str): """Handle attachment selection.""" from core.attachments import get_mock_attachments attachments = get_mock_attachments(attachment_type) if not attachments: QMessageBox.information(self, "No Attachments", f"No {attachment_type} attachments available.") return # Create simple selection dialog dialog = QDialog(self) dialog.setWindowTitle(f"Select {attachment_type.title()}") dialog.setMinimumWidth(400) layout = QVBoxLayout(dialog) list_widget = QListWidget() for att in attachments: item = QListWidgetItem(f"📎 {att.name}") item.setData(Qt.ItemDataRole.UserRole, att) list_widget.addItem(item) layout.addWidget(list_widget) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) # Add None option none_btn = QPushButton("Remove Attachment") none_btn.clicked.connect(lambda: self._clear_attachment(attachment_type) or dialog.reject()) layout.addWidget(none_btn) if dialog.exec() == QDialog.DialogCode.Accepted: selected = list_widget.currentItem() if selected: att = selected.data(Qt.ItemDataRole.UserRole) self._apply_attachment(attachment_type, att) 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 DPP calculations.""" try: config = self._get_current_config() # Update DPP dpp = config.calculate_dpp() self.dpp_label.setText(f"{dpp:.4f}") self.total_dpp_label.setText(f"{dpp:.4f}") # Update cost breakdown weapon_cost = config.calculate_weapon_cost_per_hour() armor_cost = config.calculate_armor_cost_per_hour() heal_cost = config.calculate_heal_cost_per_hour() total_cost = config.calculate_total_cost_per_hour() self.weapon_cost_label.setText(f"{weapon_cost:.0f} PEC/hr") self.armor_cost_label.setText(f"{armor_cost:.0f} PEC/hr") self.heal_cost_label.setText(f"{heal_cost:.0f} PEC/hr") self.total_cost_label.setText(f"{total_cost:.2f} PED/hr") # Update 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()} | {prot_text}") 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_hit=piece.decay_per_hit, 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_hit=plate.decay_per_hit, 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) # Set full set if all pieces match if self.current_armor_set: equipped.equip_full_set(self.current_armor_set) 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=self.shots_per_hour_spin.value(), hits_per_hour=self.hits_per_hour_spin.value(), heals_per_hour=self.heals_per_hour_spin.value(), ) def _set_config(self, config: LoadoutConfig): """Set UI fields from configuration.""" self.loadout_name_edit.setText(config.name) self.shots_per_hour_spin.setValue(config.shots_per_hour) self.hits_per_hour_spin.setValue(config.hits_per_hour) self.heals_per_hour_spin.setValue(config.heals_per_hour) # 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(name) 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.shots_per_hour_spin.setValue(3600) self.hits_per_hour_spin.setValue(720) self.heals_per_hour_spin.setValue(60) 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 name: print(f"Loadout saved: {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()