""" Lemontropia Suite - Loadout Manager UI v5.0 Full gear support with weapon amplifiers, mindforce implants, and armor platings. """ import json import os import logging from dataclasses import dataclass, field from decimal import Decimal from pathlib import Path from typing import Optional, Dict, Any from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QLabel, QPushButton, QGroupBox, QMessageBox, QListWidget, QListWidgetItem, QSplitter, QWidget, QFrame, QGridLayout, QScrollArea, ) from PyQt6.QtCore import Qt, pyqtSignal from core.nexus_full_api import get_nexus_api, NexusWeapon logger = logging.getLogger(__name__) # ============================================================================ # Full Gear Loadout Config # ============================================================================ @dataclass class LoadoutConfig: """Complete loadout configuration with full gear support. Core principle: Store all gear types needed for comprehensive cost tracking. """ # Identity name: str = "Unnamed" version: int = 3 # Version 3 = full gear support # === WEAPON & ATTACHMENTS === weapon_cost_per_shot: Decimal = Decimal("0") weapon_name: str = "None" weapon_damage: Decimal = Decimal("0") weapon_decay_pec: Decimal = Decimal("0") weapon_ammo_pec: Decimal = Decimal("0") weapon_api_id: Optional[int] = None # Weapon Amplifier weapon_amp_id: Optional[int] = None weapon_amp_name: str = "None" weapon_amp_decay: Decimal = Decimal("0") weapon_amp_damage_bonus: Decimal = Decimal("0") # === ARMOR & PLATINGS === armor_cost_per_hit: Decimal = Decimal("0") armor_name: str = "None" armor_decay_pec: Decimal = Decimal("0") armor_api_id: Optional[int] = None # Armor Plating plating_id: Optional[int] = None plating_name: str = "None" plating_decay: Decimal = Decimal("0") plating_protection_summary: str = "" # === HEALING & MINDFORCE === healing_cost_per_heal: Decimal = Decimal("0") healing_name: str = "None" healing_decay_pec: Decimal = Decimal("0") healing_api_id: Optional[int] = None # Mindforce Implant (for healing chips) mindforce_implant_id: Optional[int] = None mindforce_implant_name: str = "None" mindforce_implant_decay: Decimal = Decimal("0") mindforce_implant_heal_amount: Decimal = Decimal("0") def to_dict(self) -> dict: """Serialize to dictionary.""" return { 'name': self.name, 'version': self.version, # Weapon 'weapon_cost_per_shot': str(self.weapon_cost_per_shot), 'weapon_name': self.weapon_name, 'weapon_damage': str(self.weapon_damage), 'weapon_decay_pec': str(self.weapon_decay_pec), 'weapon_ammo_pec': str(self.weapon_ammo_pec), 'weapon_api_id': self.weapon_api_id, # Weapon Amplifier 'weapon_amp_id': self.weapon_amp_id, 'weapon_amp_name': self.weapon_amp_name, 'weapon_amp_decay': str(self.weapon_amp_decay), 'weapon_amp_damage_bonus': str(self.weapon_amp_damage_bonus), # Armor 'armor_cost_per_hit': str(self.armor_cost_per_hit), 'armor_name': self.armor_name, 'armor_decay_pec': str(self.armor_decay_pec), 'armor_api_id': self.armor_api_id, # Plating 'plating_id': self.plating_id, 'plating_name': self.plating_name, 'plating_decay': str(self.plating_decay), 'plating_protection_summary': self.plating_protection_summary, # Healing 'healing_cost_per_heal': str(self.healing_cost_per_heal), 'healing_name': self.healing_name, 'healing_decay_pec': str(self.healing_decay_pec), 'healing_api_id': self.healing_api_id, # Mindforce 'mindforce_implant_id': self.mindforce_implant_id, 'mindforce_implant_name': self.mindforce_implant_name, 'mindforce_implant_decay': str(self.mindforce_implant_decay), 'mindforce_implant_heal_amount': str(self.mindforce_implant_heal_amount), } @classmethod def from_dict(cls, data: dict) -> "LoadoutConfig": """Deserialize from dictionary with legacy support.""" version = data.get('version', 1) if version == 1: return cls._from_legacy_v1(data) elif version == 2: return cls._from_v2(data) else: return cls._from_v3(data) @classmethod def _from_v3(cls, data: dict) -> "LoadoutConfig": """Parse version 3 (current) format.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) except Exception: return Decimal(default) return cls( name=data.get('name', 'Unnamed'), version=3, # Weapon weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'), weapon_name=data.get('weapon_name', 'None'), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=get_decimal('weapon_decay_pec'), weapon_ammo_pec=get_decimal('weapon_ammo_pec'), weapon_api_id=data.get('weapon_api_id'), # Weapon Amplifier weapon_amp_id=data.get('weapon_amp_id'), weapon_amp_name=data.get('weapon_amp_name', 'None'), weapon_amp_decay=get_decimal('weapon_amp_decay'), weapon_amp_damage_bonus=get_decimal('weapon_amp_damage_bonus'), # Armor armor_cost_per_hit=get_decimal('armor_cost_per_hit'), armor_name=data.get('armor_name', 'None'), armor_decay_pec=get_decimal('armor_decay_pec'), armor_api_id=data.get('armor_api_id'), # Plating plating_id=data.get('plating_id'), plating_name=data.get('plating_name', 'None'), plating_decay=get_decimal('plating_decay'), plating_protection_summary=data.get('plating_protection_summary', ''), # Healing healing_cost_per_heal=get_decimal('healing_cost_per_heal'), healing_name=data.get('healing_name', 'None'), healing_decay_pec=get_decimal('healing_decay_pec'), healing_api_id=data.get('healing_api_id'), # Mindforce mindforce_implant_id=data.get('mindforce_implant_id'), mindforce_implant_name=data.get('mindforce_implant_name', 'None'), mindforce_implant_decay=get_decimal('mindforce_implant_decay'), mindforce_implant_heal_amount=get_decimal('mindforce_implant_heal_amount'), ) @classmethod def _from_v2(cls, data: dict) -> "LoadoutConfig": """Convert version 2 to version 3.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) except Exception: return Decimal(default) return cls( name=data.get('name', 'Unnamed'), version=3, weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'), weapon_name=data.get('weapon_name', 'None'), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=get_decimal('weapon_decay_pec'), weapon_ammo_pec=get_decimal('weapon_ammo_pec'), weapon_api_id=data.get('weapon_api_id'), armor_cost_per_hit=get_decimal('armor_cost_per_hit'), armor_name=data.get('armor_name', 'None'), armor_decay_pec=get_decimal('armor_decay_pec'), armor_api_id=data.get('armor_api_id'), healing_cost_per_heal=get_decimal('healing_cost_per_heal'), healing_name=data.get('healing_name', 'None'), healing_decay_pec=get_decimal('healing_decay_pec'), healing_api_id=data.get('healing_api_id'), ) @classmethod def _from_legacy_v1(cls, data: dict) -> "LoadoutConfig": """Convert legacy format to new format.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) except Exception: return Decimal(default) # Calculate costs from legacy fields weapon_decay = get_decimal('weapon_decay_pec') weapon_ammo = get_decimal('weapon_ammo_pec') weapon_cost_per_shot = (weapon_decay / Decimal("100")) + (weapon_ammo * Decimal("0.0001")) armor_decay = get_decimal('armor_decay_pec') armor_cost_per_hit = armor_decay / Decimal("100") heal_decay = get_decimal('heal_cost_pec') healing_cost_per_heal = heal_decay / Decimal("100") return cls( name=data.get('name', 'Unnamed'), version=3, weapon_cost_per_shot=weapon_cost_per_shot, weapon_name=data.get('weapon_name', data.get('weapon', 'None')), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=weapon_decay, weapon_ammo_pec=weapon_ammo, armor_cost_per_hit=armor_cost_per_hit, armor_name=data.get('armor_set_name', data.get('armor_name', 'None')), armor_decay_pec=armor_decay, healing_cost_per_heal=healing_cost_per_heal, healing_name=data.get('heal_name', 'None'), healing_decay_pec=heal_decay, ) def get_total_weapon_cost_per_shot(self) -> Decimal: """Calculate total weapon cost including amplifier.""" base_cost = self.weapon_cost_per_shot amp_cost = self.weapon_amp_decay / Decimal("100") # Convert PEC to PED return base_cost + amp_cost def get_total_healing_cost_per_heal(self) -> Decimal: """Calculate total healing cost including mindforce implant.""" base_cost = self.healing_cost_per_heal implant_cost = self.mindforce_implant_decay / Decimal("100") # Convert PEC to PED return base_cost + implant_cost def get_total_armor_cost_per_hit(self) -> Decimal: """Calculate total armor cost including plating.""" base_cost = self.armor_cost_per_hit plating_cost = self.plating_decay / Decimal("100") # Convert PEC to PED return base_cost + plating_cost def get_summary(self) -> Dict[str, Any]: """Get cost summary for display.""" return { 'name': self.name, 'weapon': self.weapon_name, 'weapon_amp': self.weapon_amp_name if self.weapon_amp_id else "None", 'armor': self.armor_name, 'plating': self.plating_name if self.plating_id else "None", 'healing': self.healing_name, 'mindforce': self.mindforce_implant_name if self.mindforce_implant_id else "None", 'cost_per_shot': self.get_total_weapon_cost_per_shot(), 'cost_per_hit': self.get_total_armor_cost_per_hit(), 'cost_per_heal': self.get_total_healing_cost_per_heal(), } # ============================================================================ # Full Gear Loadout Manager Dialog # ============================================================================ class LoadoutManagerDialog(QDialog): """Full-featured loadout manager with all gear types.""" loadout_saved = pyqtSignal(LoadoutConfig) def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Loadout Manager - Full Gear Configuration") self.setMinimumSize(800, 700) # State self.config_dir = Path.home() / ".lemontropia" / "loadouts" self.config_dir.mkdir(parents=True, exist_ok=True) self.current_config: Optional[LoadoutConfig] = None self._setup_ui() self._load_saved_loadouts() def _setup_ui(self): """Setup full gear UI.""" layout = QVBoxLayout(self) layout.setSpacing(10) # === Top: Loadout Name === name_layout = QHBoxLayout() name_layout.addWidget(QLabel("Loadout Name:")) self.name_edit = QLineEdit() self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt with Dante") name_layout.addWidget(self.name_edit) layout.addLayout(name_layout) # === Main Content Splitter === splitter = QSplitter(Qt.Orientation.Horizontal) # Left: Saved Loadouts left_widget = QWidget() left_layout = QVBoxLayout(left_widget) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.addWidget(QLabel("Saved Loadouts:")) self.saved_list = QListWidget() self.saved_list.itemClicked.connect(self._on_loadout_selected) self.saved_list.itemDoubleClicked.connect(self._on_loadout_double_clicked) left_layout.addWidget(self.saved_list) btn_layout = QHBoxLayout() self.new_btn = QPushButton("New") self.new_btn.clicked.connect(self._new_loadout) self.delete_btn = QPushButton("Delete") self.delete_btn.clicked.connect(self._delete_loadout) btn_layout.addWidget(self.new_btn) btn_layout.addWidget(self.delete_btn) left_layout.addLayout(btn_layout) splitter.addWidget(left_widget) # Right: Configuration (in a scroll area) scroll = QScrollArea() scroll.setWidgetResizable(True) right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(10) # -- Weapon Section -- weapon_group = QGroupBox("⚔️ Weapon & Amplifier") weapon_layout = QFormLayout(weapon_group) self.weapon_btn = QPushButton("Select Weapon...") self.weapon_btn.clicked.connect(self._select_weapon) weapon_layout.addRow("Weapon:", self.weapon_btn) self.weapon_info = QLabel("None selected") self.weapon_info.setStyleSheet("color: #888;") weapon_layout.addRow(self.weapon_info) self.weapon_cost_label = QLabel("0.0000 PED") self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") weapon_layout.addRow("Base Cost/Shot:", self.weapon_cost_label) # Amplifier self.amp_btn = QPushButton("Select Amplifier...") self.amp_btn.clicked.connect(self._select_amplifier) weapon_layout.addRow("Amplifier:", self.amp_btn) self.amp_info = QLabel("None selected") self.amp_info.setStyleSheet("color: #888;") weapon_layout.addRow(self.amp_info) self.amp_cost_label = QLabel("0.0000 PED") self.amp_cost_label.setStyleSheet("color: #FFA07A;") weapon_layout.addRow("Amp Cost/Shot:", self.amp_cost_label) self.total_weapon_cost_label = QLabel("0.0000 PED") self.total_weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") weapon_layout.addRow("Total Cost/Shot:", self.total_weapon_cost_label) right_layout.addWidget(weapon_group) # -- Armor Section -- armor_group = QGroupBox("🛡️ Armor & Plating") armor_layout = QFormLayout(armor_group) self.armor_btn = QPushButton("Select Armor...") self.armor_btn.clicked.connect(self._select_armor) armor_layout.addRow("Armor:", self.armor_btn) self.armor_info = QLabel("None selected") self.armor_info.setStyleSheet("color: #888;") armor_layout.addRow(self.armor_info) self.armor_cost_label = QLabel("0.0000 PED") self.armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") armor_layout.addRow("Base Cost/Hit:", self.armor_cost_label) # Plating self.plating_btn = QPushButton("Select Plating...") self.plating_btn.clicked.connect(self._select_plating) armor_layout.addRow("Plating:", self.plating_btn) self.plating_info = QLabel("None selected") self.plating_info.setStyleSheet("color: #888;") armor_layout.addRow(self.plating_info) self.plating_cost_label = QLabel("0.0000 PED") self.plating_cost_label.setStyleSheet("color: #FFA07A;") armor_layout.addRow("Plating Cost/Hit:", self.plating_cost_label) self.total_armor_cost_label = QLabel("0.0000 PED") self.total_armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") armor_layout.addRow("Total Cost/Hit:", self.total_armor_cost_label) right_layout.addWidget(armor_group) # -- Healing Section -- healing_group = QGroupBox("💚 Healing & Mindforce") healing_layout = QFormLayout(healing_group) self.healing_btn = QPushButton("Select Healing Tool...") self.healing_btn.clicked.connect(self._select_healing) healing_layout.addRow("Healing Tool:", self.healing_btn) self.healing_info = QLabel("None selected") self.healing_info.setStyleSheet("color: #888;") healing_layout.addRow(self.healing_info) self.healing_cost_label = QLabel("0.0000 PED") self.healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") healing_layout.addRow("Base Cost/Heal:", self.healing_cost_label) # Mindforce Implant self.mindforce_btn = QPushButton("Select Mindforce Chip...") self.mindforce_btn.clicked.connect(self._select_mindforce) healing_layout.addRow("Mindforce Chip:", self.mindforce_btn) self.mindforce_info = QLabel("None selected") self.mindforce_info.setStyleSheet("color: #888;") healing_layout.addRow(self.mindforce_info) self.mindforce_cost_label = QLabel("0.0000 PED") self.mindforce_cost_label.setStyleSheet("color: #FFA07A;") healing_layout.addRow("Chip Cost/Heal:", self.mindforce_cost_label) self.total_healing_cost_label = QLabel("0.0000 PED") self.total_healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") healing_layout.addRow("Total Cost/Heal:", self.total_healing_cost_label) right_layout.addWidget(healing_group) # -- Summary Section -- summary_group = QGroupBox("💰 Total Session Cost Summary") summary_layout = QGridLayout(summary_group) summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0) self.summary_shot = QLabel("0.0000 PED") self.summary_shot.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_shot, 0, 1) summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0) self.summary_hit = QLabel("0.0000 PED") self.summary_hit.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_hit, 1, 1) summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0) self.summary_heal = QLabel("0.0000 PED") self.summary_heal.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_heal, 2, 1) summary_layout.setColumnStretch(1, 1) right_layout.addWidget(summary_group) right_layout.addStretch() scroll.setWidget(right_widget) splitter.addWidget(scroll) splitter.setSizes([250, 550]) layout.addWidget(splitter) # === Bottom Buttons === button_layout = QHBoxLayout() button_layout.addStretch() self.save_btn = QPushButton("💾 Save Loadout") self.save_btn.clicked.connect(self._save_loadout) self.save_btn.setStyleSheet(""" QPushButton { background-color: #2E7D32; color: white; padding: 8px 16px; font-weight: bold; } QPushButton:hover { background-color: #388E3C; } """) button_layout.addWidget(self.save_btn) self.cancel_btn = QPushButton("Cancel") self.cancel_btn.clicked.connect(self.reject) button_layout.addWidget(self.cancel_btn) layout.addLayout(button_layout) def _select_weapon(self): """Open weapon selector dialog.""" from ui.weapon_selector import WeaponSelectorDialog dialog = WeaponSelectorDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: weapon = dialog.get_selected_weapon() if weapon: self._set_weapon(weapon) def _set_weapon(self, weapon: NexusWeapon): """Set weapon and calculate cost.""" # Calculate cost per shot decay_pec = Decimal(str(weapon.decay)) ammo = Decimal(str(weapon.ammo_burn)) cost_per_shot = (decay_pec / Decimal("100")) + (ammo * Decimal("0.0001")) # Update UI self.weapon_btn.setText(weapon.name[:30]) self.weapon_info.setText(f"Damage: {weapon.damage} | Range: {weapon.range_val}") self.weapon_cost_label.setText(f"{cost_per_shot:.4f} PED") # Store for saving self._pending_weapon = { 'name': weapon.name, 'api_id': weapon.id, 'damage': weapon.damage, 'decay_pec': decay_pec, 'ammo_pec': ammo, 'cost_per_shot': cost_per_shot, } self._update_weapon_total() self._update_summary() def _select_amplifier(self): """Open weapon amplifier selector.""" from ui.amplifier_selector import AmplifierSelectorDialog dialog = AmplifierSelectorDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: amp = dialog.get_selected_amplifier() if amp: self._set_amplifier(amp) def _set_amplifier(self, amp_data: dict): """Set weapon amplifier.""" name = amp_data.get('name', 'Unknown') decay_pec = Decimal(str(amp_data.get('decay_pec', 0))) damage_bonus = Decimal(str(amp_data.get('damage_bonus', 0))) cost_per_shot = decay_pec / Decimal("100") # Update UI self.amp_btn.setText(name[:30]) self.amp_info.setText(f"+{damage_bonus} Damage") self.amp_cost_label.setText(f"{cost_per_shot:.4f} PED") # Store for saving self._pending_amplifier = { 'name': name, 'api_id': amp_data.get('api_id'), 'decay_pec': decay_pec, 'damage_bonus': damage_bonus, 'cost_per_shot': cost_per_shot, } self._update_weapon_total() self._update_summary() def _update_weapon_total(self): """Update total weapon cost display.""" weapon = getattr(self, '_pending_weapon', {}) amp = getattr(self, '_pending_amplifier', {}) weapon_cost = weapon.get('cost_per_shot', Decimal("0")) amp_cost = amp.get('cost_per_shot', Decimal("0")) total = weapon_cost + amp_cost self.total_weapon_cost_label.setText(f"{total:.4f} PED") def _select_armor(self): """Open simplified armor selector.""" from ui.armor_selector import ArmorSelectorDialog dialog = ArmorSelectorDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: result = dialog.get_selected_armor() if result: self._set_armor(result) def _set_armor(self, armor_data: dict): """Set armor and calculate cost.""" name = armor_data.get('name', 'Unknown') decay_pec = Decimal(str(armor_data.get('decay_pec', 0))) cost_per_hit = decay_pec / Decimal("100") # Update UI self.armor_btn.setText(name[:30]) prot_summary = armor_data.get('protection_summary', '') self.armor_info.setText(prot_summary[:50] if prot_summary else "No protection data") self.armor_cost_label.setText(f"{cost_per_hit:.4f} PED") # Store for saving self._pending_armor = { 'name': name, 'api_id': armor_data.get('api_id'), 'decay_pec': decay_pec, 'cost_per_hit': cost_per_hit, } self._update_armor_total() self._update_summary() def _select_plating(self): """Open armor plating selector.""" from ui.plate_selector import PlateSelectorDialog dialog = PlateSelectorDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: plate = dialog.get_selected_plate() if plate: self._set_plating(plate) def _set_plating(self, plate_data: dict): """Set armor plating.""" name = plate_data.get('name', 'Unknown') decay_pec = Decimal(str(plate_data.get('decay_pec', 0))) cost_per_hit = decay_pec / Decimal("100") # Update UI self.plating_btn.setText(name[:30]) prot_summary = plate_data.get('protection_summary', '') self.plating_info.setText(prot_summary[:40] if prot_summary else "No data") self.plating_cost_label.setText(f"{cost_per_hit:.4f} PED") # Store for saving self._pending_plating = { 'name': name, 'api_id': plate_data.get('api_id'), 'decay_pec': decay_pec, 'protection_summary': prot_summary, 'cost_per_hit': cost_per_hit, } self._update_armor_total() self._update_summary() def _update_armor_total(self): """Update total armor cost display.""" armor = getattr(self, '_pending_armor', {}) plating = getattr(self, '_pending_plating', {}) armor_cost = armor.get('cost_per_hit', Decimal("0")) plating_cost = plating.get('cost_per_hit', Decimal("0")) total = armor_cost + plating_cost self.total_armor_cost_label.setText(f"{total:.4f} PED") def _select_healing(self): """Open healing selector.""" from ui.healing_selector import HealingSelectorDialog dialog = HealingSelectorDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: healing = dialog.get_selected_healing() if healing: self._set_healing(healing) def _set_healing(self, healing_data: dict): """Set healing and calculate cost.""" name = healing_data.get('name', 'Unknown') decay_pec = Decimal(str(healing_data.get('decay_pec', 0))) heal_amount = Decimal(str(healing_data.get('heal_amount', 0))) cost_per_heal = decay_pec / Decimal("100") # Update UI self.healing_btn.setText(name[:30]) self.healing_info.setText(f"Heal: {heal_amount} HP") self.healing_cost_label.setText(f"{cost_per_heal:.4f} PED") # Store for saving self._pending_healing = { 'name': name, 'api_id': healing_data.get('api_id'), 'decay_pec': decay_pec, 'heal_amount': heal_amount, 'cost_per_heal': cost_per_heal, } self._update_healing_total() self._update_summary() def _select_mindforce(self): """Open mindforce implant selector.""" from ui.mindforce_selector import MindforceSelectorDialog dialog = MindforceSelectorDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: chip = dialog.get_selected_chip() if chip: self._set_mindforce(chip) def _set_mindforce(self, chip_data: dict): """Set mindforce implant.""" name = chip_data.get('name', 'Unknown') decay_pec = Decimal(str(chip_data.get('decay_pec', 0))) heal_amount = Decimal(str(chip_data.get('heal_amount', 0))) cost_per_heal = decay_pec / Decimal("100") # Update UI self.mindforce_btn.setText(name[:30]) self.mindforce_info.setText(f"Heal: {heal_amount} HP") self.mindforce_cost_label.setText(f"{cost_per_heal:.4f} PED") # Store for saving self._pending_mindforce = { 'name': name, 'api_id': chip_data.get('api_id'), 'decay_pec': decay_pec, 'heal_amount': heal_amount, 'cost_per_heal': cost_per_heal, } self._update_healing_total() self._update_summary() def _update_healing_total(self): """Update total healing cost display.""" healing = getattr(self, '_pending_healing', {}) mindforce = getattr(self, '_pending_mindforce', {}) healing_cost = healing.get('cost_per_heal', Decimal("0")) mindforce_cost = mindforce.get('cost_per_heal', Decimal("0")) total = healing_cost + mindforce_cost self.total_healing_cost_label.setText(f"{total:.4f} PED") def _update_summary(self): """Update cost summary display.""" weapon = getattr(self, '_pending_weapon', {}) amp = getattr(self, '_pending_amplifier', {}) armor = getattr(self, '_pending_armor', {}) plating = getattr(self, '_pending_plating', {}) healing = getattr(self, '_pending_healing', {}) mindforce = getattr(self, '_pending_mindforce', {}) shot = weapon.get('cost_per_shot', Decimal("0")) + amp.get('cost_per_shot', Decimal("0")) hit = armor.get('cost_per_hit', Decimal("0")) + plating.get('cost_per_hit', Decimal("0")) heal = healing.get('cost_per_heal', Decimal("0")) + mindforce.get('cost_per_heal', Decimal("0")) self.summary_shot.setText(f"{shot:.4f} PED") self.summary_hit.setText(f"{hit:.4f} PED") self.summary_heal.setText(f"{heal:.4f} PED") def _save_loadout(self): """Save current configuration.""" name = self.name_edit.text().strip() if not name: QMessageBox.warning(self, "Missing Name", "Please enter a loadout name") return # Build config from pending data weapon = getattr(self, '_pending_weapon', {}) amp = getattr(self, '_pending_amplifier', {}) armor = getattr(self, '_pending_armor', {}) plating = getattr(self, '_pending_plating', {}) healing = getattr(self, '_pending_healing', {}) mindforce = getattr(self, '_pending_mindforce', {}) config = LoadoutConfig( name=name, # Weapon weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")), weapon_name=weapon.get('name', 'None'), weapon_damage=weapon.get('damage', Decimal("0")), weapon_decay_pec=weapon.get('decay_pec', Decimal("0")), weapon_ammo_pec=weapon.get('ammo_pec', Decimal("0")), weapon_api_id=weapon.get('api_id'), # Weapon Amplifier weapon_amp_id=amp.get('api_id'), weapon_amp_name=amp.get('name', 'None'), weapon_amp_decay=amp.get('decay_pec', Decimal("0")), weapon_amp_damage_bonus=amp.get('damage_bonus', Decimal("0")), # Armor armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")), armor_name=armor.get('name', 'None'), armor_decay_pec=armor.get('decay_pec', Decimal("0")), armor_api_id=armor.get('api_id'), # Plating plating_id=plating.get('api_id'), plating_name=plating.get('name', 'None'), plating_decay=plating.get('decay_pec', Decimal("0")), plating_protection_summary=plating.get('protection_summary', ''), # Healing healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")), healing_name=healing.get('name', 'None'), healing_decay_pec=healing.get('decay_pec', Decimal("0")), healing_api_id=healing.get('api_id'), # Mindforce mindforce_implant_id=mindforce.get('api_id'), mindforce_implant_name=mindforce.get('name', 'None'), mindforce_implant_decay=mindforce.get('decay_pec', Decimal("0")), mindforce_implant_heal_amount=mindforce.get('heal_amount', Decimal("0")), ) # Save to file safe_name = "".join(c for c in name if c.isalnum() or c in "._- ").strip() if not safe_name: safe_name = "unnamed" 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_config = config self.loadout_saved.emit(config) self._load_saved_loadouts() QMessageBox.information(self, "Saved", f"Loadout '{name}' saved!") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save: {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)) # Tooltip with costs tooltip = ( f"Weapon: {config.weapon_name}\n" f" + Amp: {config.weapon_amp_name if config.weapon_amp_id else 'None'}\n" f"Armor: {config.armor_name}\n" f" + Plating: {config.plating_name if config.plating_id else 'None'}\n" f"Healing: {config.healing_name}\n" f" + Chip: {config.mindforce_implant_name if config.mindforce_implant_id else 'None'}\n" f"Cost/Shot: {config.get_total_weapon_cost_per_shot():.4f} PED\n" f"Cost/Hit: {config.get_total_armor_cost_per_hit():.4f} PED\n" f"Cost/Heal: {config.get_total_healing_cost_per_heal():.4f} PED" ) item.setToolTip(tooltip) self.saved_list.addItem(item) except Exception as e: logger.error(f"Failed to load {filepath}: {e}") except Exception as e: logger.error(f"Failed to list loadouts: {e}") def _on_loadout_selected(self, item: QListWidgetItem): """Load selected loadout into UI.""" 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._load_config_into_ui(config) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to load: {e}") def _on_loadout_double_clicked(self, item: QListWidgetItem): """Double-click to select and close.""" self._on_loadout_selected(item) self.accept() def _load_config_into_ui(self, config: LoadoutConfig): """Load config values into UI fields.""" self.name_edit.setText(config.name) # Weapon if config.weapon_name != "None": self.weapon_btn.setText(config.weapon_name[:30]) self.weapon_info.setText(f"Damage: {config.weapon_damage}") self.weapon_cost_label.setText(f"{config.weapon_cost_per_shot:.4f} PED") self._pending_weapon = { 'name': config.weapon_name, 'api_id': config.weapon_api_id, 'damage': config.weapon_damage, 'decay_pec': config.weapon_decay_pec, 'ammo_pec': config.weapon_ammo_pec, 'cost_per_shot': config.weapon_cost_per_shot, } # Weapon Amplifier if config.weapon_amp_id: self.amp_btn.setText(config.weapon_amp_name[:30]) self.amp_info.setText(f"+{config.weapon_amp_damage_bonus} Damage") amp_cost = config.weapon_amp_decay / Decimal("100") self.amp_cost_label.setText(f"{amp_cost:.4f} PED") self._pending_amplifier = { 'name': config.weapon_amp_name, 'api_id': config.weapon_amp_id, 'decay_pec': config.weapon_amp_decay, 'damage_bonus': config.weapon_amp_damage_bonus, 'cost_per_shot': amp_cost, } # Armor if config.armor_name != "None": self.armor_btn.setText(config.armor_name[:30]) self.armor_info.setText("Loaded from save") self.armor_cost_label.setText(f"{config.armor_cost_per_hit:.4f} PED") self._pending_armor = { 'name': config.armor_name, 'api_id': config.armor_api_id, 'decay_pec': config.armor_decay_pec, 'cost_per_hit': config.armor_cost_per_hit, } # Plating if config.plating_id: self.plating_btn.setText(config.plating_name[:30]) self.plating_info.setText(config.plating_protection_summary[:40] if config.plating_protection_summary else "No data") plating_cost = config.plating_decay / Decimal("100") self.plating_cost_label.setText(f"{plating_cost:.4f} PED") self._pending_plating = { 'name': config.plating_name, 'api_id': config.plating_id, 'decay_pec': config.plating_decay, 'protection_summary': config.plating_protection_summary, 'cost_per_hit': plating_cost, } # Healing if config.healing_name != "None": self.healing_btn.setText(config.healing_name[:30]) self.healing_cost_label.setText(f"{config.healing_cost_per_heal:.4f} PED") self._pending_healing = { 'name': config.healing_name, 'api_id': config.healing_api_id, 'decay_pec': config.healing_decay_pec, 'cost_per_heal': config.healing_cost_per_heal, } # Mindforce if config.mindforce_implant_id: self.mindforce_btn.setText(config.mindforce_implant_name[:30]) mindforce_cost = config.mindforce_implant_decay / Decimal("100") self.mindforce_cost_label.setText(f"{mindforce_cost:.4f} PED") self._pending_mindforce = { 'name': config.mindforce_implant_name, 'api_id': config.mindforce_implant_id, 'decay_pec': config.mindforce_implant_decay, 'heal_amount': config.mindforce_implant_heal_amount, 'cost_per_heal': mindforce_cost, } self._update_weapon_total() self._update_armor_total() self._update_healing_total() self._update_summary() self.current_config = config def _new_loadout(self): """Clear all fields for new loadout.""" self.name_edit.clear() # Reset weapon section self.weapon_btn.setText("Select Weapon...") self.weapon_info.setText("None selected") self.weapon_cost_label.setText("0.0000 PED") self.amp_btn.setText("Select Amplifier...") self.amp_info.setText("None selected") self.amp_cost_label.setText("0.0000 PED") self.total_weapon_cost_label.setText("0.0000 PED") # Reset armor section self.armor_btn.setText("Select Armor...") self.armor_info.setText("None selected") self.armor_cost_label.setText("0.0000 PED") self.plating_btn.setText("Select Plating...") self.plating_info.setText("None selected") self.plating_cost_label.setText("0.0000 PED") self.total_armor_cost_label.setText("0.0000 PED") # Reset healing section self.healing_btn.setText("Select Healing...") self.healing_info.setText("None selected") self.healing_cost_label.setText("0.0000 PED") self.mindforce_btn.setText("Select Mindforce Chip...") self.mindforce_info.setText("None selected") self.mindforce_cost_label.setText("0.0000 PED") self.total_healing_cost_label.setText("0.0000 PED") # Clear pending data self._pending_weapon = None self._pending_amplifier = None self._pending_armor = None self._pending_plating = None self._pending_healing = None self._pending_mindforce = None self._update_summary() self.current_config = None def _delete_loadout(self): """Delete 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"Delete '{name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: os.remove(filepath) self._load_saved_loadouts() self._new_loadout() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to delete: {e}") def get_config(self) -> Optional[LoadoutConfig]: """Get current configuration.""" return self.current_config # ============================================================================ # Backward Compatibility # ============================================================================ # Keep old names for imports LoadoutManager = LoadoutManagerDialog