""" Lemontropia Suite - Loadout Manager UI A PyQt6 dialog for configuring hunting gear loadouts with cost calculations. """ import json import os from dataclasses import dataclass, asdict from decimal import Decimal, InvalidOperation from pathlib import Path from typing import Optional, List from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QComboBox, QLabel, QPushButton, QGroupBox, QSpinBox, QDoubleSpinBox, QMessageBox, QListWidget, QListWidgetItem, QSplitter, QWidget, QFrame, QScrollArea, QGridLayout ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont # ============================================================================ # Data Structures # ============================================================================ @dataclass class LoadoutConfig: """Configuration for a hunting loadout.""" name: str weapon_name: str weapon_damage: Decimal weapon_decay_pec: Decimal weapon_ammo_pec: Decimal armor_name: str armor_decay_pec: Decimal heal_name: str heal_cost_pec: Decimal # Optional fields for extended calculations shots_per_hour: int = 3600 # Default: 1 shot per second 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") def calculate_dpp(self) -> Decimal: """Calculate Damage Per Pec (DPP) = Damage / (Decay + Ammo).""" total_cost = self.weapon_decay_pec + self.weapon_ammo_pec if total_cost == 0: return Decimal("0") return self.weapon_damage / total_cost def calculate_cost_per_hour(self) -> Decimal: """Calculate total PED cost per hour based on shots per hour.""" cost_per_shot = self.weapon_decay_pec + self.weapon_ammo_pec total_weapon_cost = cost_per_shot * Decimal(self.shots_per_hour) # Estimate armor decay (assume 1 hit per 5 shots on average) armor_cost_per_hour = self.armor_decay_pec * Decimal(self.shots_per_hour // 5) # Estimate healing cost (assume 1 heal per 10 shots) heal_cost_per_hour = self.heal_cost_pec * Decimal(self.shots_per_hour // 10) total_pec = total_weapon_cost + armor_cost_per_hour + heal_cost_per_hour return total_pec / Decimal("100") # Convert PEC to PED def calculate_break_even(self, mob_health: Decimal) -> Decimal: """Calculate break-even loot value for a mob with given health.""" shots_to_kill = mob_health / self.weapon_damage if shots_to_kill < 1: shots_to_kill = Decimal("1") cost_per_shot = self.weapon_decay_pec + self.weapon_ammo_pec total_cost_pec = shots_to_kill * cost_per_shot return total_cost_pec / Decimal("100") # Convert to PED def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" return { k: str(v) if isinstance(v, Decimal) else v for k, v in asdict(self).items() } @classmethod def from_dict(cls, data: dict) -> "LoadoutConfig": """Create LoadoutConfig from dictionary.""" decimal_fields = [ 'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec', 'armor_decay_pec', 'heal_cost_pec', '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]) if 'shots_per_hour' in data: data['shots_per_hour'] = int(data['shots_per_hour']) # Handle legacy configs without heal_name if 'heal_name' not in data: data['heal_name'] = '-- Custom --' return cls(**data) # ============================================================================ # Mock Data # ============================================================================ MOCK_WEAPONS = [ {"name": "Sollomate Opalo", "damage": Decimal("9.0"), "decay": Decimal("0.09"), "ammo": Decimal("1.80")}, {"name": "Omegaton M2100", "damage": Decimal("10.5"), "decay": Decimal("0.10"), "ammo": Decimal("2.10")}, {"name": "Breer M1a", "damage": Decimal("12.0"), "decay": Decimal("0.12"), "ammo": Decimal("2.40")}, {"name": "Justifier Mk.II", "damage": Decimal("25.0"), "decay": Decimal("0.25"), "ammo": Decimal("5.00")}, {"name": "Marber Bravo-Type", "damage": Decimal("45.0"), "decay": Decimal("0.45"), "ammo": Decimal("9.00")}, {"name": " Maddox IV", "damage": Decimal("80.0"), "decay": Decimal("0.80"), "ammo": Decimal("16.00")}, ] MOCK_ARMOR = [ {"name": "Pixie Harness", "decay": Decimal("0.05"), "impact": Decimal("4"), "cut": Decimal("3"), "stab": Decimal("2")}, {"name": "Shogun Harness", "decay": Decimal("0.12"), "impact": Decimal("8"), "cut": Decimal("6"), "stab": Decimal("5"), "burn": Decimal("4")}, {"name": "Ghost Harness", "decay": Decimal("0.20"), "impact": Decimal("12"), "cut": Decimal("10"), "stab": Decimal("9"), "burn": Decimal("8"), "cold": Decimal("7")}, {"name": "Vigilante Harness", "decay": Decimal("0.08"), "impact": Decimal("6"), "cut": Decimal("5"), "stab": Decimal("4")}, {"name": "Hermes Harness", "decay": Decimal("0.15"), "impact": Decimal("10"), "cut": Decimal("8"), "stab": Decimal("7"), "penetration": Decimal("5")}, ] MOCK_HEALING = [ {"name": "Vivo T10", "cost": Decimal("2.0")}, {"name": "Vivo T15", "cost": Decimal("3.5")}, {"name": "Vivo S10", "cost": Decimal("4.0")}, {"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5")}, {"name": "Restoration Chip I", "cost": Decimal("5.0")}, {"name": "Restoration Chip II", "cost": Decimal("8.0")}, ] # ============================================================================ # 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; } """) # ============================================================================ # Main Dialog # ============================================================================ class LoadoutManagerDialog(QDialog): """Main dialog for managing hunting loadouts.""" loadout_saved = pyqtSignal(str) # Emitted when loadout is saved def __init__(self, parent=None, config_dir: Optional[str] = None): super().__init__(parent) self.setWindowTitle("Lemontropia Suite - Loadout Manager") self.setMinimumSize(900, 700) # Configuration directory 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._apply_dark_theme() self._create_widgets() self._create_layout() self._connect_signals() self._load_saved_loadouts() self._populate_mock_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; } QListWidget { background-color: #2d2d2d; color: #e0e0e0; border: 1px solid #3d3d3d; border-radius: 4px; } QListWidget::item:selected { background-color: #4a90d9; } QScrollArea { border: none; } """) def _create_widgets(self): """Create all UI widgets.""" # Loadout name self.loadout_name_edit = QLineEdit() self.loadout_name_edit.setPlaceholderText("Enter loadout name...") # Shots per hour self.shots_per_hour_spin = QSpinBox() self.shots_per_hour_spin.setRange(1, 10000) self.shots_per_hour_spin.setValue(3600) self.shots_per_hour_spin.setSuffix(" shots/hr") # Weapon section self.weapon_group = DarkGroupBox("🔫 Weapon Configuration") self.weapon_combo = QComboBox() self.weapon_combo.setEditable(False) # Show dropdown list on click self.weapon_damage_edit = DecimalLineEdit() self.weapon_decay_edit = DecimalLineEdit() self.weapon_ammo_edit = DecimalLineEdit() self.dpp_label = QLabel("0.00") self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 14px;") # Armor section self.armor_group = DarkGroupBox("🛡️ Armor Configuration") self.armor_combo = QComboBox() self.armor_combo.setEditable(False) # Show dropdown list on click self.armor_decay_edit = DecimalLineEdit() # Protection values self.protection_stab_edit = DecimalLineEdit() self.protection_cut_edit = DecimalLineEdit() self.protection_impact_edit = DecimalLineEdit() self.protection_pen_edit = DecimalLineEdit() self.protection_shrap_edit = DecimalLineEdit() self.protection_burn_edit = DecimalLineEdit() self.protection_cold_edit = DecimalLineEdit() self.protection_acid_edit = DecimalLineEdit() self.protection_elec_edit = DecimalLineEdit() # Healing section self.heal_group = DarkGroupBox("💊 Healing Configuration") self.heal_combo = QComboBox() self.heal_combo.setEditable(False) # Show dropdown list on click self.heal_cost_edit = DecimalLineEdit() # Cost summary self.summary_group = DarkGroupBox("📊 Cost Summary") self.cost_per_hour_label = QLabel("0.00 PED/hr") self.cost_per_hour_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 16px;") self.break_even_label = QLabel("Break-even: 0.00 PED (mob health: 100)") self.break_even_label.setStyleSheet("color: #4caf50;") # Break-even calculator self.mob_health_edit = DecimalLineEdit() self.mob_health_edit.set_decimal(Decimal("100")) self.calc_break_even_btn = QPushButton("Calculate Break-Even") # 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") # Refresh button for saved list 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) name_layout.addWidget(QLabel("Shots/Hour:")) name_layout.addWidget(self.shots_per_hour_spin) right_layout.addLayout(name_layout) # Weapon configuration weapon_layout = QFormLayout(self.weapon_group) weapon_layout.addRow("Weapon:", self.weapon_combo) 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("DPP (Damage Per Pec):", self.dpp_label) right_layout.addWidget(self.weapon_group) # Armor configuration armor_layout = QFormLayout(self.armor_group) armor_layout.addRow("Armor Set:", self.armor_combo) armor_layout.addRow("Decay/hit (PEC):", self.armor_decay_edit) # Protection grid protection_frame = QFrame() protection_layout = QGridLayout(protection_frame) protection_layout.setSpacing(5) protections = [ ("Stab:", self.protection_stab_edit), ("Cut:", self.protection_cut_edit), ("Impact:", self.protection_impact_edit), ("Penetration:", self.protection_pen_edit), ("Shrapnel:", self.protection_shrap_edit), ("Burn:", self.protection_burn_edit), ("Cold:", self.protection_cold_edit), ("Acid:", self.protection_acid_edit), ("Electric:", self.protection_elec_edit), ] for i, (label, edit) in enumerate(protections): row = i // 3 col = (i % 3) * 2 protection_layout.addWidget(QLabel(label), row, col) protection_layout.addWidget(edit, row, col + 1) armor_layout.addRow("Protection Values:", protection_frame) 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) right_layout.addWidget(self.heal_group) # Cost summary summary_layout = QFormLayout(self.summary_group) summary_layout.addRow("Estimated Cost:", self.cost_per_hour_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_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, 650]) main_layout.addWidget(splitter) def _connect_signals(self): """Connect all signal handlers.""" # Weapon changes self.weapon_combo.currentTextChanged.connect(self._on_weapon_changed) 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) # Armor changes self.armor_combo.currentTextChanged.connect(self._on_armor_changed) # Healing changes 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) self.shots_per_hour_spin.valueChanged.connect(self._update_calculations) # Double click on list self.saved_list.itemDoubleClicked.connect(self._load_from_item) def _populate_mock_data(self): """Populate combos with mock data.""" # Weapons self.weapon_combo.addItem("-- Custom --") for weapon in MOCK_WEAPONS: self.weapon_combo.addItem(weapon["name"]) # Armor self.armor_combo.addItem("-- Custom --") for armor in MOCK_ARMOR: self.armor_combo.addItem(armor["name"]) # Healing self.heal_combo.addItem("-- Custom --") for heal in MOCK_HEALING: self.heal_combo.addItem(heal["name"]) # Set initial enabled state (all fields enabled for custom entry) self.weapon_damage_edit.setEnabled(True) self.weapon_decay_edit.setEnabled(True) self.weapon_ammo_edit.setEnabled(True) self.armor_decay_edit.setEnabled(True) self.protection_stab_edit.setEnabled(True) self.protection_cut_edit.setEnabled(True) self.protection_impact_edit.setEnabled(True) self.protection_pen_edit.setEnabled(True) self.protection_shrap_edit.setEnabled(True) self.protection_burn_edit.setEnabled(True) self.protection_cold_edit.setEnabled(True) self.protection_acid_edit.setEnabled(True) self.protection_elec_edit.setEnabled(True) self.heal_cost_edit.setEnabled(True) def _on_weapon_changed(self, name: str): """Handle weapon selection change.""" if name == "-- Custom --": # Enable manual entry for custom weapon self.weapon_damage_edit.setEnabled(True) self.weapon_decay_edit.setEnabled(True) self.weapon_ammo_edit.setEnabled(True) # Clear fields for user to enter custom values self.weapon_damage_edit.clear() self.weapon_decay_edit.clear() self.weapon_ammo_edit.clear() else: # Auto-fill stats for predefined weapon and disable fields for weapon in MOCK_WEAPONS: if weapon["name"] == name: self.weapon_damage_edit.set_decimal(weapon["damage"]) self.weapon_decay_edit.set_decimal(weapon["decay"]) self.weapon_ammo_edit.set_decimal(weapon["ammo"]) break self.weapon_damage_edit.setEnabled(False) self.weapon_decay_edit.setEnabled(False) self.weapon_ammo_edit.setEnabled(False) self._update_calculations() def _on_armor_changed(self, name: str): """Handle armor selection change.""" if name == "-- Custom --": # Enable manual entry for custom armor self.armor_decay_edit.setEnabled(True) self.protection_stab_edit.setEnabled(True) self.protection_cut_edit.setEnabled(True) self.protection_impact_edit.setEnabled(True) self.protection_pen_edit.setEnabled(True) self.protection_shrap_edit.setEnabled(True) self.protection_burn_edit.setEnabled(True) self.protection_cold_edit.setEnabled(True) self.protection_acid_edit.setEnabled(True) self.protection_elec_edit.setEnabled(True) # Clear fields for user to enter custom values self.armor_decay_edit.clear() self.protection_stab_edit.clear() self.protection_cut_edit.clear() self.protection_impact_edit.clear() self.protection_pen_edit.clear() self.protection_shrap_edit.clear() self.protection_burn_edit.clear() self.protection_cold_edit.clear() self.protection_acid_edit.clear() self.protection_elec_edit.clear() else: # Auto-fill stats for predefined armor and disable fields for armor in MOCK_ARMOR: if armor["name"] == name: self.armor_decay_edit.set_decimal(armor["decay"]) self.protection_impact_edit.set_decimal(Decimal(armor.get("impact", "0"))) self.protection_cut_edit.set_decimal(Decimal(armor.get("cut", "0"))) self.protection_stab_edit.set_decimal(Decimal(armor.get("stab", "0"))) self.protection_burn_edit.set_decimal(Decimal(armor.get("burn", "0"))) self.protection_cold_edit.set_decimal(Decimal(armor.get("cold", "0"))) self.protection_pen_edit.set_decimal(Decimal(armor.get("penetration", "0"))) break self.armor_decay_edit.setEnabled(False) self.protection_stab_edit.setEnabled(False) self.protection_cut_edit.setEnabled(False) self.protection_impact_edit.setEnabled(False) self.protection_pen_edit.setEnabled(False) self.protection_shrap_edit.setEnabled(False) self.protection_burn_edit.setEnabled(False) self.protection_cold_edit.setEnabled(False) self.protection_acid_edit.setEnabled(False) self.protection_elec_edit.setEnabled(False) def _on_heal_changed(self, name: str): """Handle healing selection change.""" if name == "-- Custom --": # Enable manual entry for custom healing self.heal_cost_edit.setEnabled(True) # Clear field for user to enter custom value self.heal_cost_edit.clear() else: # Auto-fill stats for predefined healing and disable field for heal in MOCK_HEALING: if heal["name"] == name: self.heal_cost_edit.set_decimal(heal["cost"]) break self.heal_cost_edit.setEnabled(False) def _update_calculations(self): """Update DPP and cost calculations.""" try: config = self._get_current_config() # Update DPP dpp = config.calculate_dpp() self.dpp_label.setText(f"{dpp:.4f}") # Update cost per hour cost_per_hour = config.calculate_cost_per_hour() self.cost_per_hour_label.setText(f"{cost_per_hour:.2f} PED/hr") except Exception as e: pass # Ignore calculation errors during typing 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 health: {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.""" return LoadoutConfig( name=self.loadout_name_edit.text().strip() or "Unnamed", weapon_name=self.weapon_combo.currentText(), 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(), armor_name=self.armor_combo.currentText(), armor_decay_pec=self.armor_decay_edit.get_decimal(), heal_name=self.heal_combo.currentText(), heal_cost_pec=self.heal_cost_edit.get_decimal(), shots_per_hour=self.shots_per_hour_spin.value(), protection_stab=self.protection_stab_edit.get_decimal(), protection_cut=self.protection_cut_edit.get_decimal(), protection_impact=self.protection_impact_edit.get_decimal(), protection_penetration=self.protection_pen_edit.get_decimal(), protection_shrapnel=self.protection_shrap_edit.get_decimal(), protection_burn=self.protection_burn_edit.get_decimal(), protection_cold=self.protection_cold_edit.get_decimal(), protection_acid=self.protection_acid_edit.get_decimal(), protection_electric=self.protection_elec_edit.get_decimal(), ) 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.weapon_combo.setCurrentText(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) # Enable/disable based on whether it's a custom weapon is_custom_weapon = config.weapon_name == "-- Custom --" self.weapon_damage_edit.setEnabled(is_custom_weapon) self.weapon_decay_edit.setEnabled(is_custom_weapon) self.weapon_ammo_edit.setEnabled(is_custom_weapon) self.armor_combo.setCurrentText(config.armor_name) self.armor_decay_edit.set_decimal(config.armor_decay_pec) self.protection_stab_edit.set_decimal(config.protection_stab) self.protection_cut_edit.set_decimal(config.protection_cut) self.protection_impact_edit.set_decimal(config.protection_impact) self.protection_pen_edit.set_decimal(config.protection_penetration) self.protection_shrap_edit.set_decimal(config.protection_shrapnel) self.protection_burn_edit.set_decimal(config.protection_burn) self.protection_cold_edit.set_decimal(config.protection_cold) self.protection_acid_edit.set_decimal(config.protection_acid) self.protection_elec_edit.set_decimal(config.protection_electric) # Enable/disable based on whether it's custom armor is_custom_armor = config.armor_name == "-- Custom --" self.armor_decay_edit.setEnabled(is_custom_armor) self.protection_stab_edit.setEnabled(is_custom_armor) self.protection_cut_edit.setEnabled(is_custom_armor) self.protection_impact_edit.setEnabled(is_custom_armor) self.protection_pen_edit.setEnabled(is_custom_armor) self.protection_shrap_edit.setEnabled(is_custom_armor) self.protection_burn_edit.setEnabled(is_custom_armor) self.protection_cold_edit.setEnabled(is_custom_armor) self.protection_acid_edit.setEnabled(is_custom_armor) self.protection_elec_edit.setEnabled(is_custom_armor) self.heal_combo.setCurrentText(config.heal_name if hasattr(config, 'heal_name') else "-- Custom --") self.heal_cost_edit.set_decimal(config.heal_cost_pec) # Enable/disable based on whether it's custom healing is_custom_heal = (config.heal_name if hasattr(config, 'heal_name') else "-- Custom --") == "-- Custom --" self.heal_cost_edit.setEnabled(is_custom_heal) 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 # Sanitize filename 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)) item.setToolTip( f"Weapon: {config.weapon_name}\n" f"Armor: {config.armor_name}\n" f"DPP: {config.calculate_dpp():.2f}" ) self.saved_list.addItem(item) except Exception: continue except Exception: pass 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) self.current_loadout = 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_combo.setCurrentIndex(0) # "-- Custom --" self.armor_combo.setCurrentIndex(0) # "-- Custom --" self.heal_combo.setCurrentIndex(0) # "-- Custom --" # Clear all fields self.weapon_damage_edit.clear() self.weapon_decay_edit.clear() self.weapon_ammo_edit.clear() self.armor_decay_edit.clear() self.protection_stab_edit.clear() self.protection_cut_edit.clear() self.protection_impact_edit.clear() self.protection_pen_edit.clear() self.protection_shrap_edit.clear() self.protection_burn_edit.clear() self.protection_cold_edit.clear() self.protection_acid_edit.clear() self.protection_elec_edit.clear() self.heal_cost_edit.clear() # Enable all fields for custom entry (since "-- Custom --" is selected) self.weapon_damage_edit.setEnabled(True) self.weapon_decay_edit.setEnabled(True) self.weapon_ammo_edit.setEnabled(True) self.armor_decay_edit.setEnabled(True) self.protection_stab_edit.setEnabled(True) self.protection_cut_edit.setEnabled(True) self.protection_impact_edit.setEnabled(True) self.protection_pen_edit.setEnabled(True) self.protection_shrap_edit.setEnabled(True) self.protection_burn_edit.setEnabled(True) self.protection_cold_edit.setEnabled(True) self.protection_acid_edit.setEnabled(True) self.protection_elec_edit.setEnabled(True) self.heal_cost_edit.setEnabled(True) self.mob_health_edit.set_decimal(Decimal("100")) 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 from PyQt6.QtWidgets import QApplication 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} (DPP: {config.calculate_dpp():.2f})") print(f" Cost/hour: {config.calculate_cost_per_hour():.2f} PED") sys.exit(0) if __name__ == "__main__": main()