Lemontropia-Suite/ui/loadout_manager_simple.py

1045 lines
42 KiB
Python

"""
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